Notechondria
Version: 0.1.77 Build Date: 2026-04-26T23:00
What's Changed
Three reported bugs and one feature, follow-ups to 0.1.76. The
biggest change is replacing the is_default lock-bit semantics on
the Inbox category with name-based protection, which fixes the
"Restore default inbox" duplication bug and adds duplicate-name
prevention as a side benefit.
Bug — restore-inbox produced duplicate Inboxes
User report: tapping "Restore default inbox" twice yielded two "Inbox" categories. Root cause from 0.1.76: my fix appended a fresh Inbox course unconditionally each call, with no de-dupe.
-
Rewrote
_seedStarterInboxAlongsideExistinginfrontend/editor_app/lib/core/local_starter.dartas reuse-or-create:- Scan
_localCoursesfor a category whose title (case- insensitive) is "Inbox". If found, reuse it; if not,_buildLocalCoursea fresh one. - Welcome drafts are only seeded when the discovered Inbox has
zero drafts pointing at it (checked via
_decodeNoteMetadataon every local draft'scourse_id). - Net effect: tapping Restore N times yields exactly one Inbox and exactly two starter drafts, idempotently.
- Scan
- Log message branches between "created", "reused", and "refilled" so the debug log explains which branch fired.
Bug — is_default lock-bit on category protection
Per user direction: "Remove the hard coded locks attribute on
whether the category could be deleted, just deny all the request
for deleting the category named Inbox." The is_default boolean
on Course was being used as a lock-bit in two places (PATCH and
DELETE rejected when is_default=True); when local Inbox state
disagreed with the server (or when restore-inbox produced a
second "Inbox" without setting the bit), the protection was
inconsistent. Solution: protect by name, not by flag.
Backend
-
backend/notes/api.py:CourseDetailApiView.patchnow allows description / icon edits on the Inbox row but rejects renames away from "Inbox" (case-insensitive). Renames TO an existing other category's title are also rejected (name uniqueness on update path).CourseDetailApiView.deleterejects deletion whencourse.title.casefold() == "inbox"instead of whencourse.is_default == True.CourseListApiView.postadds a name-uniqueness guard: creating a category whose title (case-insensitive) collides with the user's existing categories returns 400. Applies on both the upsert path (client_course_idmatch) and fresh creates, withexcludeIdfor self-exclusion on rename.- Newly-created courses set
is_default = (title.casefold() == "inbox")so the flag stays accurate for downstream legacy consumers (sort key, default-fallback course resolution, the "Inbox shows public notes" filter), even though it's no longer the protection mechanism.
-
TemplateCourseRestoreApiViewis unaffected: it callsbootstrap_platformdirectly and usesCourse.objects.get_or_createinternally, so it's already idempotent and bypasses the new uniqueness check on the create endpoint.
Frontend
-
frontend/editor_app/lib/core/category_actions.dartadds two helpers and uses them throughout:_isInboxCategory(course)— single source of truth for the Inbox check. Title-based (case-insensitive); deliberately does NOT consultis_defaultso UI doesn't disagree with the backend during sync._categoryNameExists(title, {excludeId})— scans both_localCoursesand_coursesfor a duplicate, optionally skipping a self-id so renames-to-self pass.
-
_createCategorynow rejects duplicate names client-side (avoiding a server round-trip for the obvious case). -
_updateCategorynow rejects renames away from "Inbox" AND renames that would collide with another existing category. -
_deleteCategorynow uses_isInboxCategoryinstead of theis_defaultfield check. -
_unsubscribeCategory,_buildCategoryRow, and the delete-fallback "find Inbox to reassign notes" logic all switched to_isInboxCategoryso the entire frontend agrees on what makes a row the Inbox. -
_promptCreateCategorynow surfaces the duplicate-name error via SnackBar (previously it swallowedActionFeedbackresults). - Inbox protection dialog text updated to say "Inbox is the default category" instead of "This is the default category" for clarity.
Bug — "Local drafts only" filter didn't actually filter
User report: switching the dropdown to "Local drafts only"
didn't change the visible list. Root cause: scopedLocalDrafts
in core/build_helpers.dart always intersected local drafts
with the active category, so on a cloud category with no local
drafts the user saw an empty page (cloud notes hidden by scope,
local drafts empty by category-filter).
-
Fix:
core/build_helpers.dartnow drops the category filter on local drafts when_learnerSearchScope == 'local', so the "Local drafts only" choice means "show ALL local drafts across all categories" — which is the dropdown option's obvious intent. Other scopes still scope local drafts to the active category. Behavior summary documented in the helper's comment.
Feature — Bootstrap-card-style cover image on public note cards
User request: show feature image in public note cards, blog-post- card style.
-
frontend/editor_app/lib/modules/learner.dart_LearnerNoteCardnow wraps its existing content in a Column with a topNoteCoverImagebanner (21:9 aspect ratio,clipBehavior: Clip.antiAliason the Card so the corners round cleanly). The banner only shows on PUBLIC cloud notes — private cloud notes and local drafts skip it so they remain compact list rows. -
Cover URL pulls from
note['cover_image_url'](already exposed byNoteSummarySerializersince 0.1.76). When empty, the sameNoteCoverImagewidget renders the deterministic theme-colored barcode placeholder so every public card has a visual top — no awkward image-missing gap.
Files Changed
VERSION— 0.1.76 → 0.1.77.backend/notes/api.py— name-based Inbox protection (patch + delete), name-uniqueness on create, accurateis_defaultflag on new rows.frontend/editor_app/lib/core/category_actions.dart—_isInboxCategory,_categoryNameExists, swap throughout_createCategory/_updateCategory/_deleteCategory/_unsubscribeCategory/_buildCategoryRow/_promptEditCategory, surface create-failure SnackBar in_promptCreateCategory.frontend/editor_app/lib/core/local_starter.dart—_seedStarterInboxAlongsideExistingrewritten as reuse-or-create.frontend/editor_app/lib/core/build_helpers.dart—_learnerSearchScope == 'local'drops the per-category intersection on local drafts.frontend/editor_app/lib/modules/learner.dart—_LearnerNoteCardadds topNoteCoverImagebanner on public cloud notes; small whitespace shift around the existing Row.
Notes
- All four packages (editor / planner / portal / shared) pass
flutter analyzewith zero errors andflutter testsmoke suites. Backend changes are syntactically valid (ast.parse) but not run-tested locally (no Django venv); the new uniqueness guard mirrors the existing slug-uniqueness shape so it should drop in cleanly. is_defaultis still stored on the Course model — it's used for sort ordering, "Inbox shows public notes too" filtering, and finding the fallback default course after deleting another category. The lock-bit semantics are gone; the flag itself remains as a correctness hint for those legacy consumers.- Cover-image upload row in the metadata dialog is unchanged from 0.1.76 (still editor-only, owner-only, 5 MB cap).