Notechondria

Version: 0.1.76 Build Date: 2026-04-26T22:00

What's Changed

Two reported bugs and one new feature in editor_app, plus a small shared widget addition that planner_app / portal_app can adopt later.

Bug — "Restore default inbox" was a no-op when local content existed

User report: tapping "Restore starter inbox" in offline mode did nothing visible when the user already had local categories or drafts. Root cause: the maintenance action cleared the starter_workspace_seeded_at marker and called _ensureStarterWorkspace, but that helper short-circuits whenever ANY local state is present (_frontPage, _courses, _localCourses, or _localDrafts). Restore therefore only worked on a freshly-wiped workspace — exactly the scenario where the user wouldn't think to tap "Restore."

  • Fix: added _seedStarterInboxAlongsideExisting() to editor_app/lib/core/local_starter.dart. Same body as _ensureStarterWorkspace minus the empty-state guard, with appended (not overwritten) _localCourses / _localDrafts lists. The maintenance action in editor_app/lib/core/maintenance_actions.dart now calls the new helper, so the user always gets a fresh Inbox
    • 2 welcome drafts regardless of what else is in their workspace. Comment + log message updated to drop the "if missing" caveat since restore is now unconditional.

Bug — "All Notes" filter dropdown was hidden when signed out

User report: the 0.1.71 four-option filter dropdown (Personal/Private/Public/Local) was wrapped in if (widget.isAuthenticated) and disappeared entirely for anonymous users — they were stuck with only the local search bar and no way to switch between public cloud notes and local drafts.

  • Fix: editor_app/lib/modules/learner.dart now renders the dropdown unconditionally with a context-appropriate option set:
    • Authenticated: unchanged (Personal / Private / Public / Local).
    • Anonymous: new two-option list (Public notes / Local drafts only). Both map to existing backend scopes — 'all' becomes public-only since the backend filters by ownership when no token is present, and 'local' skips the cloud call entirely.
  • effectiveScope now coerces stale auth-time scopes (e.g. 'personal' cached from a previous session) to 'all' so the dropdown's value always matches one of its items — without this, Flutter throws an "unique value" assertion on the DropdownButtonFormField.
  • showCloudNotes no longer requires authentication: anonymous users with effectiveScope='all' see the public cloud results alongside their local drafts.

Feature — Note cover images (with theme-colored barcode fallback)

User request: each note instance should have an optional cover image; the user uploads it from the note metadata dialog. When no cover is set, the frontend auto-generates a barcode placeholder keyed off the note's URL/UUID. The barcode is purely a frontend render — never persisted to R2 or the CDN — and follows the active theme colors.

Backend

  • backend/notes/models.py — added Note.cover_image ImageField (upload_to='user_upload/note_covers/', blank+null). New note_cover_path() helper kept alongside note_attachment_path for symmetry, even though the model uses the simpler static-path form (instance.id may not be assigned at upload time).
  • backend/notes/migrations/0017_note_cover_image.py — new migration adding the field. No data backfill needed (existing rows get null, which the frontend reads as "no cover" → barcode fallback).
  • backend/notes/api.py:
    • NoteSummarySerializer now exposes cover_image_url (SerializerMethodField using absolute_media_url, same shape as CourseSerializer.cover_image_url). NoteDetailSerializer inherits it automatically.
    • New NoteCoverImageApiView (POST + DELETE) accepting multipart with field cover. Owner-only (403 from non-owners), 5 MB max per upload, deletes the previous file before saving the new one so storage doesn't accumulate orphans. Both methods return the updated NoteSummarySerializer payload so the client can swap the cached note row in one round-trip.
  • backend/notechondria/api_urls.py — wired notes/<int:note_id>/cover/NoteCoverImageApiView.

Frontend (editor_app)

  • frontend/editor_app/lib/core/client.dartNotechondriaClient interface gains uploadNoteCoverImage and deleteNoteCoverImage. Both signatures take a token + note id; upload also takes an XFile. Returns the updated note summary map.
  • frontend/editor_app/lib/core/http_client.dart — multipart implementations following the existing uploadNoteAttachment pattern (POST as MultipartRequest with field cover; DELETE via _httpClient.delete).
  • frontend/editor_app/lib/modules/note_metadata.dart — the metadata dialog now leads with a "Cover image" section:
    • NoteCoverImage preview (uploaded cover or barcode placeholder) at full dialog width.
    • "Upload" / "Replace" + "Remove" buttons that call openFile from file_selector (same picker the avatar upload uses) and the new client methods.
    • Inline busy spinner + error text. Buttons hide when the note isn't synced yet (id ≤ 0) or when the user is signed out, in which case the help line explains "Sync this note to the cloud before uploading a cover image."
  • frontend/editor_app/lib/modules/note_editor.dart — threads onUploadCover / onDeleteCover callbacks into _NoteMetadataDialog. Local drafts (id ≤ 0) get null callbacks so the dialog auto-degrades.
  • frontend/editor_app/lib/modules/learner.dart and frontend/editor_app/lib/core/build_helpers.dart and frontend/editor_app/lib/core/auth_flows.dart — thread the same callbacks from _AppShellState._token (when authenticated) down to the editor dialog. Three call sites: the learner page, the build-helpers builder, and the deep-link OAuth flow.
  • frontend/editor_app/lib/components/note_viewer.dart — the read-only viewer renders a NoteCoverImage above the markdown body. Cover shows in view mode but never in edit mode, matching the user spec.

Frontend (notechondria_shared)

  • frontend/notechondria_shared/lib/src/components/note_cover_image.dart — new self-contained widget. Either renders the uploaded network image (with Image.network + errorBuilder falling back to the barcode) or a deterministic Code-39-flavored barcode painter keyed off a per-note seed (uuid, falling back to title hash). Bars use colorScheme.primary; background uses colorScheme.surfaceContainerLow; caption (when shown) uses colorScheme.onSurfaceVariant — fully theme-aware. The painter uses an FNV-1a hash and a Code 39-style 9-cells-per-character bar/gap pattern with quiet zones and start/end guards so the rendered output reads as a real barcode at a glance without pulling in qr_flutter or barcode_widget (no new dependencies).
  • frontend/notechondria_shared/lib/notechondria_shared.dart — exports NoteCoverImage so editor_app (and planner_app / portal_app, when they adopt it) can use it.

Files Changed

  • VERSION — 0.1.75 → 0.1.76.
  • backend/notes/models.pyNote.cover_image + note_cover_path.
  • backend/notes/api.py — serializer field + getter + NoteCoverImageApiView.
  • backend/notes/migrations/0017_note_cover_image.py — new.
  • backend/notechondria/api_urls.pynotes/<id>/cover/ route.
  • frontend/editor_app/lib/core/local_starter.dart_seedStarterInboxAlongsideExisting().
  • frontend/editor_app/lib/core/maintenance_actions.dart — restore action calls the new helper; comment + log adjusted.
  • frontend/editor_app/lib/modules/learner.dart — context-aware dropdown items + always-visible filter row + onUploadCover / onDeleteCover props passthrough.
  • frontend/editor_app/lib/core/client.dart — interface methods.
  • frontend/editor_app/lib/core/http_client.dart — multipart implementations.
  • frontend/editor_app/lib/modules/note_editor.dart — callback fields + _openDetails plumbing.
  • frontend/editor_app/lib/modules/note_metadata.dart — cover section, picker, busy/error state.
  • frontend/editor_app/lib/components/note_viewer.dart — cover render above body.
  • frontend/editor_app/lib/core/build_helpers.dart and frontend/editor_app/lib/core/auth_flows.dart — wire cover-image callbacks at the two _NoteEditorDialog call sites that aren't inside learner.
  • frontend/notechondria_shared/lib/src/components/note_cover_image.dart (new) — shared widget with barcode painter.
  • frontend/notechondria_shared/lib/notechondria_shared.dart — export.
  • docs/TODO.md — both bug entries removed (deep-link regression-test stays since it's an open test-coverage item, not a behavior bug).

Notes

  • All four packages (editor / planner / portal / shared) pass flutter analyze with zero errors and flutter test smoke suites. Backend changes were not run-tested locally (no configured Django venv); the migration follows the existing 0015_noteattachment.py shape and the view follows the existing NoteAttachmentApiView pattern.
  • Cover-image upload is editor-only this round. planner_app and portal_app's NotechondriaClient interfaces don't have the new methods yet — they would mirror editor's two-line implementation whenever those apps grow a metadata-edit surface for notes.
  • Chrome autofill error reported by the user (autofill.service.ts:528 Did not autofill) was triaged but not addressed: the stack trace originates inside the Bitwarden browser extension, not Notechondria. If the issue is our login form not signaling autocomplete intent properly, that's a separate audit (verify autocomplete="username" / autocomplete="current-password" on the editor's login fields) — left as a TODO follow-up.