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:

  1. Delete-after-success is still data loss. Even though the local draft/course is only dropped from _localDrafts / _localCourses after the cloud createNote / createCourse call 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.
  2. Batch sync cascade. The pre-0.1.51 _syncAllLocalDrafts and _syncAllLocalCourses loops had no per-item try/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_drafts
  • notechondria.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

Verification

  • editor_app: flutter analyze 55 issues (+1 informational prefer_single_quotes from new strings; no errors). flutter test test/smoke_test.dart passes.
  • planner_app: flutter analyze 69 issues (-1 vs 0.1.50, a previously-unused private helper is now referenced); smoke tests pass.
  • portal_app: flutter analyze 67 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.dart files 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 new lib/core/local_trash.dart partial 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.