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)

  • _pickAndUploadAttachment size-check now references LocalAttachmentStore.maxBytesPerAttachment so 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, embeds record.localUrl (i.e. local://<uuid>/<filename>) into the note body, and records a compact queue entry in metadata_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: server uuid when present, else local-<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 via LocalAttachmentStore.getBytes(localUrl: \u2026) and streams them through widget.client.uploadNoteAttachment.
  • After a successful CDN upload, the note body is patched (local://\u2026 \u2192 CDN URL via content.replaceAll), the metadata queue is dropped, and the local blob is freed via store.delete(localUrl: \u2026) so device storage eventually returns to baseline.
  • Legacy compatibility: drafts still carrying bytes_base64 fall 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 three MarkdownBody call sites (live editor preview, note viewer component, note-viewer helper). For local:// URIs it fetches bytes from the store and renders via Image.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 to Image.network.
  • Switched from the deprecated imageBuilder slot to the newer sizedImageBuilder so 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, calls LocalAttachmentStore.migrateBase64Drafts(\u2026), persists the rewritten drafts when anything changed, sets local_settings['attachment_store_migrated_at'], and logs under Editor.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: _pickAndUploadAttachment rewritten; _resolveStoreNoteUuid helper added; sizedImageBuilder: _localAttachmentImageBuilder wired into the live-markdown MarkdownBody.
  • frontend/editor_app/lib/app_shell.dart: _promoteQueuedAttachments rewritten to stream bytes from LocalAttachmentStore and delete local blobs on CDN success; _migrateAttachmentStoreIfNeeded helper added; _loadLocalState invokes the shim fire-and-forget at the end.
  • frontend/editor_app/lib/components/note_viewer.dart: sizedImageBuilder: _localAttachmentImageBuilder wired.
  • frontend/editor_app/lib/core/helpers.dart: _localAttachmentImageBuilder(MarkdownImageConfig) added and plugged into the third MarkdownBody call site (note-viewer helper).

Verification

  • editor_app: flutter analyze \u2014 54 issues (up from 52; the new FutureBuilder closure 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.dart and portal_app/lib/modules/learner.dart still 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 current local_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 _WebLocalAttachmentBackend placeholder from 0.1.40 survives only for the tab's lifetime. Swap for an idb_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 _promoteQueuedAttachments is running, the body replaceAll race is mitigated by running promote inside the existing _syncLocalDraft serialization, 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 final updateNote if this surfaces in practice.