Commit Graph

10 Commits

Author SHA1 Message Date
f933b4baa4 Sync through Firebase, mirroring to GitHub, and stop re-downloading peers
Some checks are pending
release-apk / release (APK) (push) Waiting to run
Firebase becomes the primary backend when this device has been set up for it;
GitHub stays as a mirror so a device that has not moved yet still converges.
Not being set up is a normal state, not an error -- sync runs over GitHub
exactly as before, which is also the rollback.

This app hand-rolled its own pull-merge-push instead of calling syncLog, so it
had no revision tracking at all: every sync downloaded all four peer note logs
-- roughly 150 KB each -- whether or not anything had changed. It now reads one
small revision map first and skips any peer whose hash is unchanged, and
suppresses its own push when its log is unchanged. That is the single largest
traffic saving in the fleet and what keeps this inside the free tier.

sync() now takes a RemoteStore rather than a GitHubClient, so choosing a
backend is a change at the call site and nothing more.

runSync() is the one place that decides how a sync runs. The capture screen
syncs on a timer and on demand and the settings screen on a button; each used
to build its own client, so a backend change meant editing four call sites and
hoping none was missed.

Credentials are split by what is safe to publish, because this repo is public:
the Web API key and database URL ship in source (both already ship inside the
APK, and the security rules protect the data), while the account email and
password are entered per device and live in the OS keystore.

247 tests at 100% line coverage. Two assertions changed from `contains('PUT')`
to `isNotEmpty`: those tests sync twice, and the second push is now correctly
suppressed as a no-op -- that suppression is the point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJ9sxEw3ymzXNjL7SHNpJc
2026-08-04 23:36:24 +02:00
66143bdfe3 Make the app compile and run for web, behind platform seams
First step of moving the desktop app off Flutter's Linux embedder, which tops
out at ~20fps at 4K (see docs/desktop-performance-findings.md). Android is
unaffected and keeps every existing code path.

The blocker was that `dart:io` was imported by note_repository, capture_screen
and settings_screen. On web it compiles to a stub and then throws at runtime --
the first web build rendered a blank white window. So each `dart:io` use now
sits behind a conditional export, with the shared logic hoisted into pure Dart:

- note_repository loses `dart:io`/sqlite entirely. Platform wiring moves to
  repository_factory{,_io,_web}, and the legacy sqlite import moves to its own
  io-only library. The HLC-from-updated_at seeding is preserved verbatim, so
  independently-migrating devices still resolve pre-cutover edits by real edit
  time.
- Notes persist to IndexedDB on web, not localStorage: the log is the primary
  copy of the backlog and localStorage caps at ~5-10MB and is evicted eagerly.
- BACKLOG.md and the explicit Export keep working through a local wrapper
  (added next). A browser cannot write `~/todo/BACKLOG.md`, and the user's
  tooling plus the todo MCP server read that path, so leaving it to rot was not
  an option. Writes are best-effort so the app still works with no wrapper.
- Notes are also mirrored to the wrapper's on-disk copy, so a wiped Chrome
  profile is not a total-loss event for notes that have not reached GitHub yet.

`openRepository` is split so `openRepositoryIn` keeps the migration tests; the
suite stays at 174 green and 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 13:14:03 +02:00
f463e50d67 Switch to very_good_analysis, matching the rest of the Flutter ecosystem
Standardizes on the same stricter lint set + shared analysis_options.yaml
used by diet-guard/app and the new habit_stack app. Fixes all 127 issues
surfaced: dart fix --apply for imports/cascades/sorting, unawaited() on
fire-and-forget Futures, bare catch(e) narrowed to on Exception catch(e)
(matching diet_guard's existing pattern), one documented ignore for a
deliberate TypeError catch around untrusted peer JSON, and missing
public-member doc comments throughout. No behavior change: 174/174 tests
pass, 100% line coverage maintained.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 11:05:13 +02:00
0be07060de Replace the stock Flutter launcher icon, add a Linux desktop icon
Adopt the shared icon family (python_pkg/app_icons in testsAndMisc): a
charcoal #1B1D21 field carrying a deep-purple checklist glyph, deep purple
being this app's own theme seedColor. Two ticked-off rows rather than a lone
checkmark, so it reads as a notes app and not as a generic "done" badge.

Android gains the adaptive-icon layers the project never had, including an
Android 13 monochrome layer.

Linux needs separate handling: Flutter's GTK runner never sets a window icon,
so my_application.cc now calls gtk_window_set_icon_name and
install_desktop_entry.sh populates the hicolor theme from the pre-rendered
PNGs in linux/icons (committed, so the installer needs no image tooling).
StartupWMClass is the APPLICATION_ID rather than the binary name - verified
with xprop against a running window, where WM_CLASS is the GApplication id.

No Dart code changes, so test coverage is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 17:01:32 +02:00
253088c4ed Use crdt_sync's shared GitHubClient; drop todo's local copy
crdt_sync v0.3.0 now provides canAccessRepo() + deleteFile(), so todo syncs
through the one shared client like the other apps instead of its own copy.
SyncService loses its SHA-tracking (crdt_sync's putFileText self-resolves the
sha); lib/sync/github_client.dart + its test are deleted; capture/settings/
sync_smoke repoint to the shared client. flutter analyze clean, 169 tests at
100% coverage, device-verified (27-note backlog intact, no crash).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
2026-07-11 23:53:47 +02:00
b9d9ac6521 Migrate notes storage from sqlite_crdt to the shared crdt_sync LogStore
NoteRepository is rewritten on crdt_sync's LogStore: each Note is stored *as*
a Record (per-field LWW + sticky tombstone), so the on-disk log IS the sync
wire format. Its public API is unchanged (upsert/delete/listNotes/watchNotes/
watchCount/importNotes/changes/nodeId), so the UI and most tests are
untouched; only the internals, the two sync methods (getChangeset/merge ->
exportLog/importLog over a Log), and the changeset-specific tests change.
Filtering/sorting now run in Dart over the in-memory log; watchNotes/watchCount
seed synchronously in onListen to avoid a subscription race.

SyncService moves from per-device sqlite changesets to per-device crdt_sync
Logs under a new todo-sync/notes/ path (clean format cutover; old clients
can't misread a new file). todo's own GitHubClient transport is kept.

One-time migration in NoteRepository.open: reads the legacy sqlite DB (kept
untouched as a safety net; the dep stays only for this), seeds every note --
tombstones included -- into the log with each field's HLC derived from its
updated_at (so a pre-cutover edit still wins by real edit time across two
independently-migrating devices), reuses the legacy node id, and sets a
persisted "migrated" flag so it never re-runs.

Verified: flutter analyze + dart format clean; 178 tests at 100% coverage
(incl. migration, tombstone, restore-race, and corrupt-peer cases). Device
verified on dev.kuhy.todo: real backlog of 27 live notes (+5 tombstones)
migrated intact -- confirmed against a pulled sqlite backup, the pulled
LogStore JSON, and the app's own "27 saved" -- notes render with status/
priority/dates, no crash, migration one-time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
2026-07-11 23:28:54 +02:00
9841aecd32 Store the GitHub sync token in the OS keystore, migrating off plaintext
The token moved from plaintext SharedPreferences into flutter_secure_storage
(Android Keystore / libsecret). Only the non-secret owner/repo/clientId stay in
prefs. Migration is confirm-before-delete: load() reads the keystore first and
falls back to the legacy plaintext token, migrating it only once a secure write
succeeds; save() likewise keeps writing plaintext if no secret service is
available, so we degrade to — never below — the old behaviour.

157 tests, 100% line coverage, analyze clean. Verified on-device: Settings
stays connected and sync succeeds after the one-time migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:57:05 +02:00
6db9ee11d0 Auto-export a local Markdown backup and recover from it on launch
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>
2026-06-15 22:11:08 +02:00
7f84414c87 Add list filters/sort, status, priority rework, export/import, structured template
Notes list & filtering:
- Text-search filter plus independent date-range filters for both created
  and last-updated (AND-combined), a priority filter, and a new status
  filter. Default view hides Done/Abandoned and renders as "unfiltered"
  (no badge for the default state); fixed badge clipping.
- NoteSort options wired into the list UI; watchCount() for the "N saved".

Status & priority:
- New Status enum (toDo/inProgress/Done/Abandoned) as a settable + filterable
  attribute on every note, with capture-screen dropdown.
- Removed "None" priority: every note is Low/Medium/High, default Medium.
  Schema migration v2->v3 rewrites legacy priority 0 -> Medium.

Export / import:
- NotesMarkdown round-trippable single-file format with HTML-comment markers.
- Settings "Export notes" (mobile share sheet / desktop writes ~/todo/BACKLOG.md)
  and "Import notes" (file picker + safe newer-wins merge by id).

Structured template:
- Every new note pre-fills the richer what/where/must/nice/out/done/depends/
  estimate/refs scaffold.

Tests:
- New fast (~5s), deterministic suite via FakeNoteRepository (no DB timers) and
  injected http/file-selector/url-launcher fakes. 86 tests, 96.2% line coverage
  (note.dart & sync_service.dart at 100%, settings 98.7%). Mobile-only share
  branch excluded via coverage:ignore (unreachable on the Linux test host).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:52:59 +02:00
d48bcd24f7 Initial commit: offline-first CRDT notes app (capture + GitHub sync)
Flutter app for Android + Linux desktop. Captures ideas with per-keystroke local autosave to a CRDT-backed SQLite store (sqlite_crdt), and syncs through a private GitHub repo using per-device changeset files (conflict-free last-writer-wins merge). Includes GitHub OAuth device-flow sign-in with PAT fallback, a barebones notes list, and sync settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:20:19 +02:00