The capture screen fed the on-disk BACKLOG.md backup off an always-live
watchNotes() subscription, so every keystroke re-queried the whole table,
re-parsed every row, and rebuilt the full Markdown export on the UI
isolate — O(notes) per character. It also fired a whole-screen setState
just to update the "Saved locally" timestamp.
- Add NoteRepository.changes: an O(1) broadcast tick after each write
(upsert/delete/merge; importNotes transitively). LocalBackup now pulls
notes via an injected fetcher only when its 2s debounce fires, so the
query + parse + Markdown export runs at most once per idle window
instead of on every keystroke. Capture listens to changes, not
watchNotes; a launch-time schedule() keeps BACKLOG.md fresh.
- Scope the save indicator to a ValueNotifier/ValueListenableBuilder so a
keystroke updates one line of text, not the AppBar/dropdowns/editor.
- Key list tiles by note id for identity-based reorder diffing.
Measured on a 4K/144Hz desktop the UI-thread build stays <=3.3ms;
coverage held at 100% on all changed files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Third durability layer beside GitHub auto-sync and Android Auto Backup:
a plain, human/LLM-readable Markdown file kept current on local disk.
- LocalBackup (lib/sync): pure, injectable file IO. scheduleExport()
debounces writes (a burst of keystrokes → one export); recover() parses
the file back into notes. Reused NotesMarkdown serializer.
- CaptureScreen wires it: on launch, recover into an *empty* DB only (so a
stale backup never clobbers existing notes), then keep the backup current
as notes change. Platform path = ~/todo/BACKLOG.md on desktop (the path
the user's workflow already reads) or the app documents dir on mobile
(covered by Android Auto Backup). File IO is injected in tests.
- Added fake_async dev dep to unit-test the debounce with a virtual clock.
151 tests, 100% line coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>