Commit Graph

12 Commits

Author SHA1 Message Date
5c1bf75bf9 Bulk note writes and assign-to-repo helper
NoteRepository.bulkUpdate sets status/priority on many notes in one
store write (mergeLogs + replaceAll; only changed fields plus updated_at
get a clock). saveReplacing saves one note and tombstones others in one
write. assignToRepo moves plain notes into the repo's open note as
"## title" sections via addRepoTask, demoting body headings below "##"
so a template note stays one section; notes already in a repo are
refused. The seeded watch stream moved to log_watch.dart for the
250-line cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWXdsB7CQPNXVN829G1
2026-11-10 15:38:54 +01:00
57019eec30 Track who filed a note (created_by)
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
New optional note field created_by (kuhy|claude), stored like by/repo and
omitted when unset so older builds stay wire-compatible. App UI stamps kuhy;
MCP add_task and the CLI stamp claude. list/get/search output includes it.
Human notes sort above Claude's; Claude notes are hidden behind a per-device
eye toggle. CLI --backfill-created-by [--dry-run] classifies old notes by the
node that wrote created_at, writing only the created_by field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 14:18:19 +01:00
9349747629 Repo tasks: repo/done_check/by/evidence fields, filter chips, CLI close/list/search
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Four optional note fields (repo, done_check, by, evidence) in the crdt_sync
record, written only when set, so editing a plain idea stays wire-identical
to v3 (note_repository_sync_compat_test). The log is schemaless: no stored
schema version; the legacy sqlite reader stays at v3.

NoteFilter gains a sources facet (ideas / every repo task / one repo);
search also matches the repo. The notes list shows only open ideas by
default (no badge); the filter sheet adds Ideas, Repo tasks and one chip
per repo when repo tasks exist. The per-note sheet shows the task fields
read-only. BACKLOG.md markers carry the new attributes, entity-escaped.

CLI: --repo --done-check --by on add, --close <id> --evidence (refused
without), --list [--repo] [--status], --search, and every id flag takes a
unique 8-character prefix. --list prints id8, status, priority, repo, title.

install_hooks.sh: the wrapper finds pre-commit's generated hook next to
itself, so commits from a git worktree (where .git is a file) work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 08:26:26 +01:00
d145f92a07 Split note_repository.dart, taking coverage to 100%
429 -> 249 lines:

  note_query.dart   NoteSort, NoteFilter, and the pure predicates behind
                    them (matchesFilter, comparatorFor). All of it takes
                    notes and a filter and returns a decision, which is what
                    made it the natural seam.
  note_import.dart  ImportOutcome and the in-memory LogPersistence used by
                    openInMemory.

note_repository.dart re-exports both, so no call site changed.

Also drops ImportOutcome.total, which nothing called. It was uncovered
before this split too -- the larger file's percentage hid it. With it gone
the suite reports 100% line coverage (2005 lines), up from the 99.9%
baseline measured at the start of this refactor.

lib/ and bin/ are now fully under the cap. 313 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 16:32:06 +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
313368530a Restore notes at their own edit time, not "now"
Backup recovery and file import both wrote restored notes through `upsert`,
which stamps a fresh clock. Under crdt_sync's per-field last-writer-wins that
makes a *restore* outrank every other device: the restoring device claims it
edited every field just now, so it silently overwrites newer edits made
elsewhere on the next sync.

This is not hypothetical. The new desktop app seeded itself from BACKLOG.md and
rewrote 245 of 330 field clocks to today under a fresh node id, then synced --
taking authority over the phone for ~74% of all fields. Content happened to
survive (the copy came from the same data; only a trailing newline on 5 notes
differed via the Markdown round-trip), but nothing about the mechanism
guaranteed that.

The legacy sqlite migration already did the right thing for exactly this
reason, so the fix is to use that same helper everywhere a note is *restored*
rather than newly written. Renamed legacyRecordFor -> recordAtNoteTime, since
it is no longer migration-specific.

The freshness check in importNotes is unchanged: it decides whether the import
wins *locally*. The clock decides whether it wins on *other* devices, and an
import must not win there merely by being recent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 14:24:13 +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
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
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
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