Notechondria
Version: 0.1.49 Build Date: 2026-04-20T10:00
What's Changed
Editor sidebar: Delete vs Unsubscribe by ownership
When a user long-presses / right-clicks a subscribed cloud
category they don't own, the sidebar used to show Delete,
which hit DELETE /courses/<id>/ and bounced back as a raw 403
("You do not have permission to perform this action") — not the
shape the user wanted to see.
The fix branches on ownership in the edit dialog:
- Owned category (local or cloud where
is_owned == true): existing behavior. Rename + icon + Delete visible. Deleting moves notes to the default category and callsDELETE /courses/<id>/. - Subscribed category owned by another user: rename / icon /
Save hidden (those would also 403 server-side). The destructive
action label flips to Unsubscribe and calls
DELETE /courses/<id>/subscribe/via the existingclient.unsubscribeCoursemethod. On success the category is dropped from the sidebar; the course itself stays on the server so the user can resubscribe later.
Ownership is decided in _promptEditCategory before the dialog
renders:
final isOwned = _isLocalCourse(course) || course['is_owned'] == true;
Local (negative-id) categories are always owned; for cloud
courses we trust the is_owned flag that _decorateRemoteCourse
writes when the authenticated user's username matches the
course's owner.username. When ownership is unclear (no
authenticated session at decoration time) we default to the
read-only branch so the UI can't produce a backend 403.
Flow — unsubscribe path
- Long-press category row →
_promptEditCategory(course). - Dialog detects
isOwned == false, shows subscribed-view copy- Unsubscribe button.
- User taps Unsubscribe →
_confirmWithDelay(...)prompt. - On confirm →
_unsubscribeCategory(course)(new helper) →client.unsubscribeCourse(token, courseId)→ removes the row from_courses, re-selects the default category if the unsubscribed one was active, logs shape-§1.7 line underEditor.Sync.Courses/unsubscribe.
TODO.md maintenance
Deleted three stale entries that were already shipped:
- Invalid-token session-clear + bind short-circuit for planner and portal (landed 0.1.46).
- Offline-mode toggle (landed 0.1.46 — a finer-grained follow-up remains noted under App preferences).
- Editor "..." menu restructure (landed 0.1.46 — a follow-up to port the same pattern to planner/portal inline editors replaced the old entry).
Also removed the just-landed Category ownership entry.
Files Changed
New
docs/versions/0.1.49.md(this file).
Modified
VERSION: 0.1.48 → 0.1.49.- frontend/editor_app/lib/app_shell.dart:
_promptEditCategorycomputesisOwnedand routes the newunsubscribeaction; new_unsubscribeCategoryhelper (§1.7-shaped telemetry + feedback);_EditCategoryDialoggainsisOwnedprop and branches its body + actions. - docs/TODO.md: stale entries removed.
Verification
editor_app/planner_app/portal_app:flutter analyzeissue counts unchanged vs 0.1.48 (54 / 70 / 68); no errors.flutter test test/smoke_test.dartpasses on all three.
Notes / follow-ups
- Planner + portal parity not needed here. The Delete-vs-Unsubscribe mismatch was editor-specific — planner and portal already surface unsubscribe on their course detail pages, not via a sidebar long-press menu.
is_ownedis frontend-derived. The backendCourseSerializerstill doesn't emit anis_ownedfield;_decorateRemoteCoursecompares username strings locally. If we ever need server-side enforcement (e.g. hiding the Delete button in a future admin context), addis_ownedto the serializer.