Commit Graph

14 Commits

Author SHA1 Message Date
c71b98c08b Desktop: no stray browser window from extensions; keyboard stays down after backing out of a note
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
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 12:00:26 +02:00
9f2ffdb8a9 Attach any file to a note, of any size, not only images
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
📎 now picks any file (Android: Camera / Gallery / File; desktop: an
unfiltered dialog) and the desktop page accepts any dropped file. Photo and
screenshot formats up to 64 MB are still re-encoded and shown inline;
everything else is stored byte for byte on dufs beside the images and linked
as [name](url), shown as a row with its size that opens in the platform's
app (Android) or a new browser tab (desktop). BACKLOG.md rewrites file links
to ~/data/cloud/todo-images/ like image links.

There is no size cap, so no layer holds a file in memory: uploads and
downloads stream through DufsImageClient, writes land in a .tmp renamed when
whole (an interrupted download no longer leaves a truncated file that looks
done), the desktop page POSTs the browser's own Blob with fetch, and Android's
"File" uses file_picker, because file_selector's Android side reads the whole
file into memory on the UI thread. Verified with a 1 GB file end to end on
desktop (peak RSS +36 MB) and on an API 36 emulator.

The wrapper serves attachments from the origin that holds the GitHub token
and the notes, so only script-free types open inline (nosniff), the rest
download; writes carrying another site's Origin are refused (this also
closes cross-site overwrites of the backup files); and attachment routes run
concurrently so a long upload or a playing video cannot stall the page.

Also: cupertino_icons 2.0.0 (unused, but the freshness gate blocks the
pubspec otherwise), open_file 4.0.0 and file_picker 13.1.0 pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:17:06 +02:00
05056cf8c2 Rename ImageRef to AttachmentRef ahead of non-image attachments
A ref is about to name any file stored under todo-images/, not only a
re-encoded JPEG. Mechanical rename of the type and its file; no behaviour
change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:59:34 +02:00
2bbfe0b663 Desktop: launch Chrome with --password-store=basic so a locked keyring cannot blank the window
Some checks failed
dependency freshness / dependencies on newest stable (push) Has been cancelled
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 16:31:08 +02:00
0798f7b1d0 Attach images to notes, stored on dufs and kept offline
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 14:11:15 +02:00
753e410fb4 Export the backlog to ~/src/todo, not the pre-reorganisation ~/todo
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
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 17:13:21 +02:00
a0d37c4df8 Re-point paths for the ~ reorganisation (home-tidy)
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
Also bumps Flutter 3.47.3, flutter_secure_storage 11.1.0, idb_shim 2.9.9,
sqflite_common_ffi 2.4.3, very_good_analysis 11 and mcp 2.2.0 so the
freshness gate lets this through; v11 adds async_return_with_no_await, so
the 18 flagged return sites now await explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwtowrowDXx6ccmWVPkCaF
2026-09-11 19:49:03 +02:00
381afd1d77 Split the last three oversized lib/bin files
github_mirror_screen.dart  368 -> 232 + device_code_dialog.dart (109) and
                             a widget part
  wrapper_server.dart        336 -> 223 + wrapper_sync_routes.dart, the two
                             credential-provisioning routes
  bin/todo_desktop.dart      253 -> 199 + lib/desktop/browser_launcher.dart

The fixed port moved into browser_launcher.dart as kWrapperPort, with the
entry point aliasing it, so the "must not move" reason is stated once next
to the code that acts on it.

lib/ and bin/ are now entirely under the cap. The two remaining analyze
infos (bin/todo_desktop.dart cascade_invocations,
capture_screen_sync.dart unnecessary_unawaited) are pre-existing --
verified by analyzing e0eb8b9 in a worktree, which reports the same two.

Verified: 313 tests pass, coverage unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 16:29:35 +02:00
9d2aa1c178 Enforce the 250-line file cap, and split the first four files
Wires the shared gate (~/utils/scripts/check_file_length.sh) into a new
.pre-commit-config.yaml as a local hook, checking only the files in the
commit and failing rather than warning. No baseline, no allowlist.

The repo had a hand-written .git/hooks/pre-commit running two checks;
both are ported verbatim into the config rather than dropped, with the
version bump ordered last so it cannot rewrite pubspec.yaml for a commit
a gate is still about to reject. scripts/install_hooks.sh now installs
pre-commit (and removes the legacy hook) instead of writing its own.

.github/workflows/file-length.yml runs the same check on push, kept
separate from release-apk.yml so an over-long file fails lint rather than
blocking an APK release.

Splits the four cleanest violations, all behaviour-preserving:

  note_template.dart      370 -> 203 + parser (186)
  firebase_backend.dart   308 -> barrel (24) + project/store/client
  port_guard.dart         315 -> 232 + linux_system_probe (100)
  notes_list_screen.dart  698 -> 225 + 5 widget/nav files

Where a file had external callers, the original path stays as a barrel
that re-exports the split parts, so no call site changed.

Verified: 313 tests pass, coverage identical at 99.9% / 1922 lines
(per-file too -- notes_list's 281 covered lines are now 81+75+51+41+22+11),
flutter analyze clean, and a staged 260-line file makes git commit fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 16:01:24 +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
be376fd240 Make the sync-account route testable, and cover every branch
Some checks are pending
release-apk / release (APK) (push) Waiting to run
The route read its own configuration from Platform.environment and $HOME, so
the only branch a test could reach was "disabled" -- the serving path, the
missing-config path and the malformed-email path were all unreachable without
mutating the process environment.

Both are now constructor parameters defaulting to the environment, so
production behaviour is unchanged and tests drive all four outcomes: 404 when
disabled, 200 with the account when enabled, 404 when ~/.config/crdt-sync/ is
incomplete, and 404 when firebase.json has no usable email. The password's
trailing newline is stripped, which the test pins.

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 21:49:16 +02:00
3c6c071513 Let the desktop provision its sync account from the wrapper
The desktop app is a Flutter web build in a Chrome --app window, so
FlutterSecureStorage resolves to the web backend: AES-encrypted localStorage
keyed inside the browser profile. That left the sync account settable only by
typing it into the settings screen — nothing outside the page can write that
store (verified: `secret-tool` targets libsecret, which only a Linux build
would read, and a Dart CLI cannot even import flutter_secure_storage since it
is a Flutter plugin that fails to resolve on the VM).

So the account now arrives the same way the GitHub token already does: from
the wrapper, which owns the filesystem and serves the page. loadAccount()
falls back to GET /sync-account when the store is empty and persists the
result, so the route is consulted once per install rather than being
load-bearing forever.

The route reads ~/.config/crdt-sync/{firebase.json,password} — the same pair
the Python daemons already use — so there is one source of truth per machine
instead of a per-app copy.

Off unless CRDT_SYNC_SERVE_ACCOUNT is set, because it hands a credential with
database write access to anything that can reach the port. Verified both
ways against the running wrapper: 404 without the env var, 200 with it, and
the app serves normally either way. It answers 404 rather than 403 when
disabled, so a probe cannot tell the route exists.

clearAccount() now writes an opt-out marker that the fallback checks —
without it, Disconnect would delete the account and the next launch would
silently re-adopt it, making the button look broken.

252 tests, analyzer clean. Pinned to crdt_sync_dart-v0.8.0 for the shared
path/env-var constants and accountFromWrapper().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177xwzcpfpWG2qca8Mr39q9
2026-08-09 21:39:16 +02:00
e2dfc2652e Recover instead of crashing when port 8730 is already taken
Launching the app died with an unhandled SocketException whenever anything
already held the wrapper port -- from `todo` and from `./run.sh` alike, since
run.sh execs the same entry point. What was holding it was a wrapper from an
earlier session that had outlived its window: /proc/<pid>/exe read
"/opt/todo/bin/todo_desktop (deleted)", i.e. install_arch.sh had replaced the
binary underneath a process that kept serving anyway.

That orphan is manufactured by the handoff branch in bin/todo_desktop.dart.
When the launched browser exits within 5s the launcher assumes it handed the
URL to an existing window and keeps serving rather than pulling the server out
from under it -- right at that moment, but nothing ever reaps the server once
that window closes. It then survives an upgrade and blocks every later launch.

The port cannot simply move: the browser keys localStorage (the GitHub token)
and IndexedDB (the notes) by origin, so a fallback port would present an empty,
logged-out app. lib/desktop/port_guard.dart therefore decides who owns 8730 and
acts: a wrapper with no window attached is stale, so it is terminated
(SIGTERM, escalating to SIGKILL) and the port rebound with the current build; a
wrapper that still has a window is joined, leaving exactly one server; anything
else is named and refused. Reaping is what makes an upgrade self-healing, so
install_arch.sh needs no restart step of its own -- one mechanism, not two.

It fails closed at every step. `ss` hides the owning pid for another user's
socket, and that is a real branch rather than an impossible one: it resolves to
"refuse", never to a blind kill or a retry loop.

Two things measurement contradicted, both of which had to change the design:

Re-running the Chrome command does not focus the existing --app window, it
opens a second one (verified on Thorium: the process returns in ~180ms and the
window count goes 1 -> 2). So the join path opens a window instead of printing
and exiting; both windows share the one server and one IndexedDB, and typing
`todo` to get nothing visible would be the worse outcome.

Chrome also rewrites its own argument vector into a single space-joined string,
so /proc/<pid>/cmdline for a browser contains no NUL separators at all. Exact
token matching therefore never fired, and the first run of the live-window case
reaped the server under an open window. Both matchers now tolerate either
layout. The direction of that error matters and is commented as such: a false
negative kills a server someone is using, a false positive merely opens a
spare window.

The decision logic lives in lib/ rather than in bin/, which is
coverage:ignore-file -- otherwise the one path that has to work would be the
only untested code in the repo. Host access sits behind an injected SystemProbe,
and the ss parser is additionally exercised against genuine `ss` output by
binding a real socket, since a fixture cannot catch a format change.

Verified by running it, not only by tests: orphaned dev wrapper, orphaned AOT
wrapper from the installed package, live window, a foreign owner (python
http.server -- named, exit 1, and confirmed still alive afterwards), ./run.sh,
and plain `todo`. 238 tests, 100% line coverage held.

CLAUDE.md picks up the port guard, and drops two stale claims: the suite is 238
tests taking 1-2min, not 191 tests in ~5s -- that drift predates this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:13:15 +02:00
c37a9ef118 Add the desktop wrapper, and stop the test suite eating BACKLOG.md
The desktop app is a browser now, so it cannot write files. This adds the other
half: a loopback HTTP server that serves the web build and owns the two files
the browser cannot touch -- `~/todo/BACKLOG.md`, which the user's tooling and
the todo MCP server read, and a disk copy of the note log so a wiped Chrome
profile is not a total-loss event for unsynced notes.

Notable details:

- Binds to 127.0.0.1 only. The endpoints overwrite files in the user's home
  with no authentication, so a routable bind would let anything on the network
  rewrite the backlog.
- Serves explicit content types. CanvasKit refuses a `.wasm` labelled anything
  else and the app then renders nothing at all.
- Mirroring to disk is debounced. The store persists on every keystroke, so
  mirroring eagerly meant an HTTP request plus a whole-log write per character;
  IndexedDB still gets every keystroke, only the durability copy waits.

Separately, this fixes a pre-existing destructive bug found while testing:
`settings_screen_test` did real file I/O and overwrote the user's canonical
`~/todo/BACKLOG.md` with a single fake note on *every* `flutter test` run. It
went unnoticed because the app rewrites the file whenever it launches. The
export destination is now injectable and the test redirects it to a temp dir;
verified that a full suite run leaves the real file byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 13:33:25 +02:00