Notechondria
Version: 0.1.41 Build Date: 2026-04-19T11:00
What's Changed
Attachment CDN rework, commit 2 of 3: editor wiring
Replaces the editor's inline data:<ct>;base64,\u2026 attachment
queue with the shared LocalAttachmentStore that landed in 0.1.40.
The editor now stores picked files as real blobs under
<app_support>/notechondria/attachments/<note_uuid>/<filename>
(native) or an in-memory map (web), embeds a compact
local://<note_uuid>/<filename> URL into the note markdown, and
promotes those to CDN URLs on the first successful sync.
Picker path (modules/note_editor.dart)
_pickAndUploadAttachmentsize-check now referencesLocalAttachmentStore.maxBytesPerAttachmentso the cap is owned in one place.- Cloud-ready path (saved note + session + upload callback) unchanged, except it now falls through to the local-store queue on any upload failure.
- Local-draft / no-token / upload-failed path completely rewritten:
calls
LocalAttachmentStore.open() \u2192 put(\u2026)to persist bytes, embedsrecord.localUrl(i.e.local://<uuid>/<filename>) into the note body, and records a compact queue entry inmetadata_json['queued_attachments']with{filename, content_type, size_bytes, local_url, note_uuid, queued_at}. No base64 in the markdown body anymore. - New helper
_resolveStoreNoteUuid()picks the key: serveruuidwhen present, elselocal-<client_draft_id>so local-only drafts share the same namespace pattern as the migration shim. - Messages follow AGENTS.md \u00a71.7 shape
(
Editor.UI/editor.attachment,Editor.Sync.Notes/attachment.queue).
Promote path (app_shell.dart)
_promoteQueuedAttachments(note, metadata)no longer decodes base64; it reads bytes from the store viaLocalAttachmentStore.getBytes(localUrl: \u2026)and streams them throughwidget.client.uploadNoteAttachment.- After a successful CDN upload, the note body is patched
(
local://\u2026\u2192 CDN URL viacontent.replaceAll), the metadata queue is dropped, and the local blob is freed viastore.delete(localUrl: \u2026)so device storage eventually returns to baseline. - Legacy compatibility: drafts still carrying
bytes_base64fall back to the 0.1.37 decode-and-upload path so a pre-migration queue still works.
Preview path (core/helpers.dart)
- New
_localAttachmentImageBuilder(MarkdownImageConfig)wired into the threeMarkdownBodycall sites (live editor preview, note viewer component, note-viewer helper). Forlocal://URIs it fetches bytes from the store and renders viaImage.memory; non-image bytes fall back to a small pill with the filename; missing / evicted entries render a warning pill naming the attachment. Non-local URIs fall back toImage.network. - Switched from the deprecated
imageBuilderslot to the newersizedImageBuilderso flutter_markdown's explicit width / height is respected.
Migration shim (_loadLocalState follow-up)
- New host method
_migrateAttachmentStoreIfNeeded()invoked fire-and-forget at the end of_loadLocalState. Walks_localDrafts, callsLocalAttachmentStore.migrateBase64Drafts(\u2026), persists the rewritten drafts when anything changed, setslocal_settings['attachment_store_migrated_at'], and logs underEditor.LocalStore/attachment_store_migrate. Subsequent boots short-circuit on the marker. - Runs off the critical boot path (
unawaited) so the first paint isn't blocked by a disk walk; the shim itself is idempotent.
Files Changed
New
docs/versions/0.1.41.md(this file).
Modified
VERSION: 0.1.40 \u2192 0.1.41.frontend/editor_app/lib/modules/note_editor.dart:_pickAndUploadAttachmentrewritten;_resolveStoreNoteUuidhelper added;sizedImageBuilder: _localAttachmentImageBuilderwired into the live-markdownMarkdownBody.frontend/editor_app/lib/app_shell.dart:_promoteQueuedAttachmentsrewritten to stream bytes fromLocalAttachmentStoreand delete local blobs on CDN success;_migrateAttachmentStoreIfNeededhelper added;_loadLocalStateinvokes the shim fire-and-forget at the end.frontend/editor_app/lib/components/note_viewer.dart:sizedImageBuilder: _localAttachmentImageBuilderwired.frontend/editor_app/lib/core/helpers.dart:_localAttachmentImageBuilder(MarkdownImageConfig)added and plugged into the thirdMarkdownBodycall site (note-viewer helper).
Verification
editor_app:flutter analyze\u2014 54 issues (up from 52; the newFutureBuilderclosure introduces 2 info-level hints). No errors.flutter test test/smoke_test.dart -r compact\u2014 passes.planner_app/portal_app: issue counts unchanged vs 0.1.40 (70 / 68). Smoke tests pass \u2014 the new shared dependency doesn't regress anything.notechondria_shared:flutter test -r compact\u2014 15 tests pass (6 archive + 9 attachment store from 0.1.40).
Notes / follow-ups
Remaining work in the three-commit attachment CDN plan:
- Commit 3 \u2014 planner + portal wiring + attachments list
widget. The inlined editors in
planner_app/lib/modules/learner.dartandportal_app/lib/modules/learner.dartstill use the 0.1.37 base64 queue path. Replicate the editor pattern there. Also add a compact "Attachments" list widget under the editor toolbar showing every currentlocal_url+ CDN URL embedded in the note body, with per-row delete + preview. This round deferred that widget to keep the diff focused. - IndexedDB web backend. The in-memory
_WebLocalAttachmentBackendplaceholder from 0.1.40 survives only for the tab's lifetime. Swap for anidb_shim-backed implementation once the editor wiring is validated in a staging build. - Storage-budget surface.
LocalAttachmentStore.totalBytes()is not yet wired into any UI; a later round can surface it in the settings / debug log card. - Race avoidance on concurrent sync. If the user edits a
draft while
_promoteQueuedAttachmentsis running, the bodyreplaceAllrace is mitigated by running promote inside the existing_syncLocalDraftserialization, but under heavy editing it could still drop a just-added queue entry. Track and add a compare-and-swap on the metadata before the finalupdateNoteif this surfaces in practice.