Notechondria
Version: 0.1.51 Build Date: 2026-04-21T11:00
What's Changed
Data-loss fix: local recycle bin + per-item batch-sync isolation
User-reported bug: "When user upload their local files to the server, their local copy was removed, but the upload process may fail — this leads to loss of data." Two underlying problems were addressed:
- Delete-after-success is still data loss. Even though the
local draft/course is only dropped from
_localDrafts/_localCoursesafter the cloudcreateNote/createCoursecall returns 200, the user has no way to recover if the cloud copy later turns out wrong (wrong course_id remap, botched metadata, corrupted content, or just a response that was misleading). The local copy is gone the instant the 200 lands. - Batch sync cascade. The pre-0.1.51
_syncAllLocalDraftsand_syncAllLocalCoursesloops had no per-itemtry/catch. If draft #1 synced and was removed, then draft #2 threw, the loop aborted before draft #3 was attempted. Draft #1 was already persisted as "gone"; drafts #3+ never got a sync attempt in that round. A mid-loop server glitch could cascade.
Both fixes land in this round across all three apps.
Client-side recycle bin
Two new SharedPreferences buckets added in every app's
_LocalAppStore:
notechondria.local_trashed_draftsnotechondria.local_trashed_courses
After _syncLocalDraft / _syncLocalCourse confirms the cloud
create/update returned 200, the local copy is moved to the
trash bucket (tagged with trashed_at ISO timestamp + the
returned server_note_id / server_course_id / uuid) instead
of being discarded.
_LocalAppStore.load() auto-prunes entries older than 30 days on
each startup via _pruneTrashed, so the bucket can't grow
unbounded. Entries without a parseable timestamp are kept (we
don't silently delete data we can't date).
Restore UI
A new "Synced drafts (recoverable) (N)" button now appears in the
Settings page of every app. Tap it to open a bottom-sheet that
lists every trashed draft and trashed course with a Restore
button. Restoring a draft copies the payload back to _localDrafts
with a fresh local id so it never collides with the surviving
cloud copy — the user ends up with both: the cloud note AND a
local working copy they can compare / edit / re-sync. Same pattern
for categories. Each restore sheet item also shows how long ago
the entry was trashed (e.g. "12m ago", "3d ago"), so the user can
spot a recent bad sync at a glance.
_clearLocalData now also wipes the trash buckets — that's the
"nuke everything" path and the user's explicit intent there is a
clean slate.
Per-item try/catch in batch-sync loops
_syncAllLocalDrafts and _syncAllLocalCourses in all three apps
now wrap each iteration in try/catch. On failure the item stays
in _localDrafts / _localCourses and the loop continues to the
next item. Each failure emits a §1.7-shaped warning log:
Local draft not synced: <App>.Sync.Notes/push \u2014
'<title>' (<cause>). Kept locally; will retry on next sync.
This is a strict improvement: the pre-fix cascade abort was pure loss (later items silently un-attempted).
Urgent: file-size-limit rule added to TODO.md
The user added a TODO.md item flagging that app_shell.dart has
grown past a reasonable size and proposing a new AGENTS.md rule:
no code file >1000 lines. This is marked Urgent and will be
handled as a separate dedicated round — the sync-path fix in
this round is scoped to the data-loss bug. The three app_shell.dart
files are now ~5200 / ~4100 / ~3900 lines and need splitting
regardless; the recycle-bin code added ~300 lines per app should
be folded into whatever module extraction lands in 0.1.52.
Files Changed
New
docs/versions/0.1.51.md(this file).
Modified
VERSION: 0.1.50 → 0.1.51.- frontend/editor_app/lib/core/local_store.dart,
frontend/planner_app/lib/core/local_store.dart,
frontend/portal_app/lib/core/local_store.dart:
new trashedDrafts/trashedCourses snapshot fields,
_trashedDraftsKey_trashedCoursesKeySharedPreferences keys,_trashTtlDays+_pruneTrashedload-time pruner,saveTrashedDrafts/saveTrashedCourses.
- frontend/editor_app/lib/app_shell.dart,
frontend/planner_app/lib/app_shell.dart,
frontend/portal_app/lib/app_shell.dart:
new
_localTrashedDrafts/_localTrashedCoursesstate, loaded in_loadLocalState;_moveDraftToLocalTrash/_moveCourseToLocalTrashhelpers called from_syncLocalDraft/_syncLocalCourseafter successful cloud create; per-itemtry/catchin_syncAllLocalDrafts+_syncAllLocalCourses;_restoreTrashedDraft/_restoreTrashedCourse+_openLocalRecycleBinDialog+_formatTrashedAtfor the restore UI;_clearLocalDatanow wipes trash buckets too. - frontend/editor_app/lib/modules/settings.dart,
frontend/planner_app/lib/modules/settings.dart,
frontend/portal_app/lib/modules/settings.dart:
new
onOpenLocalRecycleBin+localTrashedDraftCount+localTrashedCourseCountconstructor params; "Synced drafts (recoverable) (N)" button in the maintenance button row.
Verification
editor_app:flutter analyze55 issues (+1 informational prefer_single_quotes from new strings; no errors).flutter test test/smoke_test.dartpasses.planner_app:flutter analyze69 issues (-1 vs 0.1.50, a previously-unused private helper is now referenced); smoke tests pass.portal_app:flutter analyze67 issues (-1 for same reason); smoke tests pass.
Notes / follow-ups
- Urgent file-size rule. TODO.md now carries an Urgent entry
to pull the rule from AGENTS.md and split every file over
1000 lines. The three
app_shell.dartfiles are the primary targets; this round intentionally did not split them because the data-loss fix was the higher priority. The recycle-bin helpers added here are natural candidates to land in a newlib/core/local_trash.dartpartial once the split happens. - Attachment uploads already use the correct "delete after success" pattern (0.1.40–0.1.42), but they still discard the local blob outright on CDN success. Consider extending the recycle-bin bucket to cover local attachments too, with the same 30-day TTL. Deferred because attachment storage volumes can be much larger than drafts/courses and the trust level for CDN returns is higher.
- Cross-app visibility. The three recycle-bin buckets use the same SharedPreferences keys, which means editor / planner / portal running on the same device share the trash list. This is the intended behavior — a draft synced by the editor can be restored from the planner if something went wrong — but worth noting so the operator understands the sharing.