Commit Graph

17 Commits

Author SHA1 Message Date
49a94c9550 Migrate Sync settings to the shared sync_settings_ui package
Some checks failed
release-apk / release (APK) (push) Has been cancelled
Splits the old single Settings screen into two: "Enable advanced" plus
links to a Sync settings route (Firebase + notes backup, now the shared
sync_settings_ui package used across every kuhy app that syncs) and an
Advanced sync (GitHub) route. GitHub stays app-local rather than moving
into the shared package: connecting it here also triggers a real note sync
via runSync(), not just a settings save, which the generic package
intentionally doesn't do for any app.

313/313 tests passing, 100% line coverage on both new/changed screens,
flutter analyze clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KdSp6Nbrs9xvsWkCMDN9a1
2026-08-12 23:25:16 +02:00
ce2ea5b410 Simplify capture UI behind an advanced-mode toggle, add usage analytics
Hide priority/status/template/mode chrome and routine sync-status text
behind a Firebase-synced "Enable advanced" setting (default off); remove
the Capture title and Save button in favor of autosave + a plain reset
icon; sort notes by priority then recency; add interaction-only usage
analytics buffered locally and flushed to Firebase alongside note sync.

Code review (self + follow-up verification pass) found and fixed 3 real
bugs during this work: a widget-key placement bug that discarded an
in-progress draft when advancedMode was toggled mid-typing, a data-loss
bug in the analytics buffer's flush-and-clear logic under concurrent
cap-eviction, and two spots where an expired/wrong-account Firebase
session (FirebaseAuthError) escaped a narrower catch clause and would
have failed an otherwise-successful sync pass.

Also fixes an unrelated pre-existing test-isolation bug in
wrapper_server_test.dart, which was reading the real
~/.config/todo/firebase_auth.json instead of a test-scoped path.

320 tests, 100% line coverage on all touched files (one pre-existing,
out-of-scope 5-line gap in settings_screen.dart unrelated to this
change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 21:30:40 +02:00
d316563015 Fix desktop Firebase sync: seed a Google-derived session, not a dead password
Some checks are pending
release-apk / release (APK) (push) Waiting to run
The account's password grant is retired fleet-wide (see link_google.py /
seed_session.py in ~/utils/crdt-sync), so desktop's Firebase form could never
have connected regardless of provisioning. Desktop-as-web can't use the
in-browser Google Sign-In button either (google_platform_web.dart hard-codes
googleSignInSupported = false), so it needs the same session-seeding path the
Python daemons already use: seed_session.py now includes todo, and the
wrapper's new /sync-credentials route serves that seeded session to
loadAccount() once per launch, then closes for the rest of the process.

Also stops the status line reporting a bare "Synced" while running over the
GitHub mirror alone with Firebase disconnected -- SyncResult now carries
firebaseConnected so the UI can say "GitHub-only (Firebase not connected)".

Includes the prior session's Google Sign-In one-tap addition
(google_sign_in_backend.dart and friends), which this builds on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPsyKQRX8ZwJpUqg37jp83
2026-08-12 19:35:18 +02:00
85ba556af2 Sync on a Firebase-only device, and say why loadAccount failed
Some checks are pending
release-apk / release (APK) (push) Waiting to run
Fleet audit for the bugs found on diet-guard's desktop; two were live here.

`_autoSync` and the manual `_sync` both gated on `settings.isConfigured`,
which means "has a GitHub token". A device connected only to Firebase would
therefore never sync and would be told to "Add a GitHub token" -- advice that
is wrong now and will be wrong for every device once the mirror retires. Both
accept either backend, and the prompt names Firebase first.

This only escaped notice because this machine happens to hold a GitHub token;
diet-guard's desktop, which does not, never synced at all.

`loadAccount()` also swallowed its exception whole, hiding why a device could
not read its stored account -- indistinguishable from "no account set", and
the single most useful diagnostic when provisioning fails. It now logs at
error level with the stack trace and still returns null, so the settings
screen behaves exactly as before.

The unconfigured-Sync test now asserts the routing (unconfigured -> settings)
rather than one snackbar phrasing, which is the behaviour that actually
matters and does not break on wording.

256 tests, analyzer clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177xwzcpfpWG2qca8Mr39q9
2026-08-09 22:29:03 +02:00
ea76a04388 Add the Firebase connect UI, without which the cutover could never happen
Some checks failed
release-apk / release (APK) (push) Has been cancelled
The Firebase backend has been wired up since the cutover commit, but nothing
ever called saveAccount(). openFirebase() therefore read an account that was
never written, returned null every time, and the app quietly synced over
GitHub forever -- on real hardware, regardless of what the tests said. The
same hole was found and fixed in diet-guard; it is still open in
home_inventory, workout_app, build_your_x and leetcode-guard.

Settings now has a Firebase section that stores the account and signs in
immediately, so a wrong password shows up on screen instead of as a silent
no-op later. A rejected sign-in clears the account rather than leaving it
half-stored.

The three UI problems kuhy hit in diet-guard are avoided rather than
repeated: GitHub moves into "Advanced (GitHub mirror)" instead of sitting
beside Firebase as a peer choice, and a connected account renders as
read-only text with a Disconnect button rather than an empty-looking form
that implies you still have to type something.

The keystore accessors are injectable, like firebaseFactory already was, so
the connect/reject/disconnect branches are testable without a platform
channel. 252 tests, back to 100% line coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017FH32BgGQbQLDstBhR26Xa
2026-08-05 09:29:41 +02:00
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
7564a6569f Surface sync outcomes so devices can't silently diverge
The desktop and phone split-brained for 5 days (39 vs 30 notes) after the
crdt_sync cutover because one device kept running the pre-migration build
— and nothing in the UI ever said a sync had failed or skipped a peer:

- SyncResult now reports peer files that were listed but couldn't be
  fetched/decoded (skippedFiles) instead of dropping them silently.
- Auto-sync no longer swallows errors: every sync (manual or automatic)
  records a persisted last-sync status shown in a status line under the
  editor, with failures in the error color.
- Desktop actually background-syncs now: focus loss (inactive/hidden,
  debounced 5s) triggers a sync — Flutter Linux rarely delivers the
  `paused` event the old code relied on. `paused` still syncs immediately
  for Android.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5C5jZ2c4mTaGedK7C1dDv
2026-07-16 17:58:41 +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
01411810da Cut per-keystroke O(N) work to keep capture smooth at scale
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>
2026-07-11 21:51:46 +02:00
5ca289ca81 Rework guided mode from Stepper to full-screen per-step view
Replace the vertical Stepper widget with a full-screen single-step
view: progress bar, label, helper text, and a tall TextField that
expands to fill the available height. Navigation is Next/Back buttons
only; the final step's Next becomes Done which exits to Raw.

Removes _stepKeys (scroll-into-view keys) and the async _goToStep
(Future.delayed + Scrollable.ensureVisible) since neither is needed
without a collapsing Stepper. _goToStep is now synchronous.

Nav buttons sit below an Expanded area so they stay above the soft
keyboard when resizeToAvoidBottomInset resizes the Scaffold.

Tests: replace find.byType(Stepper) with find.byType(LinearProgressIndicator),
rewrite the navigation test, add three new tests (progress counter,
last-step Done, Done exits to Raw).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Cb7oE5Xsc8zSBHHtT6qwa
2026-06-27 12:15:19 +02:00
29f94e76a5 Add priority/template wizard and raw-as-default for guided mode
Entering Guided on an empty draft now runs a two-step wizard (priority,
then template) before showing the stepper. CaptureScreen defaults to Raw
mode so a quick capture stays in the single-field flow; switching to
Guided opens the wizard. Chrome (template/mode bar and priority/status
row) is hidden while Guided or the wizard is active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Cb7oE5Xsc8zSBHHtT6qwa
2026-06-27 12:10:21 +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
085287861c Auto-sync on launch and on app background
Manual sync left a large window in which local edits had not reached
the remote, and a reinstalled device only recovered after a manual
sync. Make sync automatic so the GitHub repo (the durable store) stays
near-current and recovery is near-automatic.

- Pull on launch (once settings load) so a reinstalled device pulls its
  old changeset back without user action.
- Push when the app is backgrounded (AppLifecycleState.paused) so edits
  leave the device promptly. Lifecycle-based, not per-keystroke, to keep
  CPU/battery and GitHub API usage low.
- Best-effort: silent, skips when unconfigured, single-flight guard so a
  launch sync and a background sync never overlap; failures are swallowed
  (the manual Sync button still surfaces errors).

100% line coverage maintained.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:02:39 +02:00
abd4ba3bd7 Add full note view/editor with templates and Markdown render
Notes were previously only openable via a quick-actions sheet; you
could not read or edit a note in full. Add a shared NoteEditor used by
both the capture and detail screens, plus selectable templates and a
rendered Markdown view.

- note_template.dart: pure assemble/parse layer over a Markdown subset
  (# title, ## sections + italic guidance, dropping empty sections).
  assemble(parse(text)) is idempotent for conforming text; non-conforming
  / legacy / freeform text is reported so the UI falls back to raw,
  untouched. Two templates: llm-design-spec (default) and blank.
- note_editor.dart: View / Guided / Raw modes. Guided is an inline
  Stepper (one step per section with its guidance); View renders the
  note via MarkdownView; Raw is the verbatim text. Guided is offered
  only for structured templates; switching to it is blocked when the
  raw text no longer conforms.
- markdown_view.dart: lean read-only renderer for the note subset,
  wrapped in a SelectionArea for copy-out.
- note_detail_screen.dart: full-screen note; opens in View, edits
  persist immediately, priority/status dropdowns, delete.
- capture_screen / notes_list_screen wired to the new editor and detail
  screen (tap a note opens it; quick actions move to the overflow button).

The editor is a view over plain text, so CRDT storage and Markdown
export/sync are unaffected. 138 tests, 100% line coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:59:31 +02:00
6947757ba0 Reach 100% test coverage (capture-screen sync DI + plugin/clipboard fakes)
- Inject an optional http.Client into CaptureScreen (mirroring SettingsScreen)
  so the configured sync path runs against a MockClient instead of the network;
  capture_screen.dart now 100%.
- Mock the file_selector and url_launcher platform interfaces and the clipboard
  channel so the import flow, _openPage launch, and the device-code dialog's
  error/Cancel/Open paths are exercised deterministically (no hangs, no timers).
- Add unit tests for the remaining fallbacks/defaults: copyWith no-arg paths,
  GitHubApiException.toString, default-constructed clients, empty NoteFilter,
  the v1->v2 status-column migration, and the export/import error branches.
- coverage:ignore the private static-only NotesMarkdown ctor.

101 tests, all green in ~5.5s. Line coverage 96.2% -> 100%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:11:01 +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