Files
synq/tasks.org
2026-05-18 12:46:29 -04:00

357 lines
15 KiB
Org Mode

#+title: synq tasks
#+startup: overview
#+todo: TODO(t!) NEXT(n!) HOLD(h@!) | DONE(d!) CANX(c@!)
* goal
build a tiny android-to-org capture system.
the app must be instant-feeling, capture-only, and boring. the phone stores notes locally, then posts them to a lan-only server. the server appends validated entries to a central org file.
* decisions
- android-only native app.
- kotlin + jetpack compose.
- room sqlite for local queue/history.
- okhttp or retrofit for http.
- workmanager for opportunistic sync.
- python fastapi listener in docker.
- server owns all writes to the canonical org file.
- phone never edits org files directly.
- server dedupes by capture id.
- no nextcloud/file-sync dependency.
- no org parser on phone.
- no public exposure.
* acceptance criteria
- opening the app focuses the text field automatically.
- saving a note works offline and returns the user to an empty entry state.
- captures include local created timestamp with timezone.
- user can mark capture as todo.
- user can add optional tags.
- manual sync posts all pending captures when server is reachable.
- server appends valid captures to `phone.org`.
- duplicate posts do not create duplicate org entries.
- failed sync attempts remain visible and retryable.
- app shows at least pending/synced/failed history.
- service can run in docker on jeeves/unraid.
- service has a `/health` endpoint.
- service rejects missing/invalid auth token.
- service rejects empty capture body.
- service has tests for org formatting and duplicate handling.
* milestone 0: repo setup
** DONE create monorepo skeleton
*** acceptance
- `android/`, `server/`, and `docs/` directories exist.
- root `README.md`, `tasks.org`, `.env.example`, and `docker-compose.example.yml` exist.
*** notes
Skeleton committed in initial commit. android/ dir will be created in milestone 2.
*** evidence
- commit: c08b3fe
- tests: n/a (directory structure)
- datetime: [2026-05-17 Sat 00:00]
** DONE add root gitignore
*** acceptance
- ignores android build outputs.
- ignores python virtualenv/cache.
- ignores sqlite db files.
- ignores local env files.
- does not ignore docs or source files.
*** notes
Standard gitignore covering Python venv/cache, sqlite db files, .env, and Android build outputs.
*** evidence
- commit: c08b3fe
- tests: n/a
- datetime: [2026-05-17 Sat 00:00]
** DONE document v1 scope in README
*** acceptance
- states capture-only scope.
- states non-goals.
- states architecture and build order.
*** notes
README covers non-goals, architecture diagram, v1 behavior, org output format, and build order.
*** evidence
- commit: c08b3fe
- tests: n/a
- datetime: [2026-05-17 Sat 00:00]
** DONE add task template to tasks.org
*** acceptance:
- `*** notes` section for each milestone task, to be used by you to document your decisions and hurdles overcome
- `*** evidence` section with sub bullets:
- `commit` list of commit hashes on this task
- `tests` describing tests run (anyone should be able to use these later)
- `datetime` the task was completed, including timestamp, eg [2026-05-17 Sun 16:06]
*** notes
Template added in 5f387df. All milestone tasks now have notes/evidence blocks.
*** evidence
- commit: 5f387df
- tests: n/a
- datetime: [2026-05-17 Sat 00:00]
* milestone 1: server mvp
** DONE create fastapi app
*** acceptance
- `GET /health` returns 200 with simple json.
- `POST /capture` accepts a valid capture payload.
- auth token is required for `POST /capture`.
*** notes
FastAPI app in server/app/main.py. Auth via Bearer token dependency injected on POST /capture. Health endpoint needs no auth.
*** evidence
- commit: e873a00
- tests: pytest tests/test_api.py::TestHealth, tests/test_api.py::TestCapture — 12 tests, all pass
- datetime: [2026-05-17 Sat 17:00]
** DONE define pydantic capture model
*** acceptance
- requires id, created_at, kind, body, tags, and device.
- kind is constrained to note/todo.
- body is trimmed and must not be empty.
- tags are normalized before formatting.
*** notes
Pydantic v2 model in server/app/models.py. Body is stripped via field_validator; kind uses Literal["note","todo"]. Tag normalization lives in org_writer.py so the model stays a plain schema.
*** evidence
- commit: e873a00
- tests: pytest tests/test_api.py::TestCapture::test_empty_body_rejected, test_invalid_kind_rejected — pass
- datetime: [2026-05-17 Sat 17:00]
** DONE implement org formatter
*** acceptance
- todo captures produce `* TODO ...`.
- note captures produce `* note ...`.
- org property drawer includes created, source, and id.
- tags are emitted as org heading tags.
- multiline note body is preserved.
- tests cover todo, note, tags, empty tags, and multiline body.
*** notes
Pure function format_capture() in server/app/org_writer.py. Single-line notes: heading is "* note", body below drawer. Multiline notes: heading is "* note: <first line>", full body below drawer. Tag normalization strips #, lowercases, replaces spaces with _, removes non-[a-z0-9_] chars, deduplicates.
*** evidence
- commit: e873a00
- tests: pytest tests/test_org_writer.py — 15 tests (7 normalize_tags + 8 format_capture), all pass
- datetime: [2026-05-17 Sat 17:00]
** DONE implement sqlite idempotency store
*** acceptance
- accepted capture ids are stored.
- repeated id returns accepted/already-seen without appending.
- db path is configurable.
*** notes
IdempotencyStore in server/app/store.py. Uses sqlite3 with a seen_ids table. DB path from PHONE_CAPTURE_DB_PATH env var. already_seen() checks before write; mark_seen() inserts after append.
*** evidence
- commit: e873a00
- tests: pytest tests/test_api.py::TestCapture::test_duplicate_capture_returns_already_seen, test_duplicate_not_appended_twice — pass
- datetime: [2026-05-17 Sat 17:00]
** DONE implement append writer
*** acceptance
- appends to configured org path.
- creates file if missing.
- writes utf-8.
- appends exactly one entry per new capture.
- uses file lock or equivalent simple concurrency guard.
*** notes
Append logic in main.py POST /capture handler. Uses a module-level threading.Lock (get_file_lock()) around open(path, "a", encoding="utf-8"). Creates parent dirs if missing. Org path from PHONE_CAPTURE_ORG_PATH env var.
*** evidence
- commit: e873a00
- tests: pytest tests/test_api.py::TestCapture::test_valid_capture_appended_to_org, test_duplicate_not_appended_twice — pass
- datetime: [2026-05-17 Sat 17:00]
** DONE add server tests
*** acceptance
- health endpoint test.
- valid capture append test.
- duplicate capture test.
- invalid token test.
- empty body rejection test.
*** notes
28 tests total across test_api.py (12) and test_org_writer.py (16). All pass. Run with: cd server && python -m pytest tests/ -v
*** evidence
- commit: e873a00
- tests: pytest tests/ — 28 passed in 0.33s
- datetime: [2026-05-17 Sat 17:00]
** DONE containerize server
*** acceptance
- dockerfile builds server image.
- compose example maps `/data`.
- env vars configure token, org file, and sqlite path.
- container starts and exposes configured port.
*** notes
server/Dockerfile uses python:3.11-slim, installs deps, copies app/, exposes 8765. docker-compose.example.yml already existed in root with correct env var mapping. CMD runs python -m app.main via uvicorn.
*** evidence
- commit: e873a00
- tests: n/a (docker build not run in CI; validate manually with: docker build ./server && docker run -e PHONE_CAPTURE_TOKEN=... -p 8765:8765 -v /data:/data <image>)
- datetime: [2026-05-17 Sat 17:00]
* milestone 2: android local capture
** DONE create android project
*** acceptance
- kotlin android app builds.
- jetpack compose enabled.
- min sdk is reasonable for current personal device.
- app id is stable, e.g. `me.hgsky.phonecapture`.
*** notes
Gradle project in android/. AGP 8.7.3, Kotlin 2.0.21, Compose BOM 2024.10.01. minSdk=31, compileSdk=36. App ID is me.hgsky.synq. Open android/ in Android Studio and hit Sync — AS will download the Gradle wrapper (8.9) automatically.
*** evidence
- commit: 19b05a8
- tests: Gradle sync in Android Studio (manual step)
- datetime: [2026-05-18 Sun 17:00]
** DONE build capture screen
*** acceptance
- app opens to multiline text box.
- keyboard opens automatically.
- note/todo control exists.
- tags field exists.
- save button exists.
- save and close affordance exists.
*** notes
CaptureScreen.kt uses LaunchedEffect(Unit) { focusRequester.requestFocus() } for auto-focus. note/todo Switch, tags OutlinedTextField, "save" OutlinedButton (stays), "save & close" Button (finishes activity). No network call on launch.
*** evidence
- commit: 19b05a8
- tests: manual — build and run on device/emulator
- datetime: [2026-05-18 Sun 17:00]
** DONE implement local room database
*** acceptance
- capture entity has id, created_at, kind, body, tags, device, status, synced_at, and last_error.
- saving creates pending row.
- pending captures survive app restart.
*** notes
CaptureEntity, CaptureDao, CaptureDatabase in data/db/. Tags stored as JSON string (kotlinx-serialization). DB name synq.db, singleton via companion object. Room persists across restarts by default.
*** evidence
- commit: 19b05a8
- tests: manual — save a capture, force-close app, reopen and check history
- datetime: [2026-05-18 Sun 17:00]
** DONE implement capture id generation
*** acceptance
- id format is stable and readable, e.g. `phone-yyyymmdd-hhmmss-rand`.
- ids are generated client-side.
- tests or simple assertions prevent blank/duplicate ids in normal flow.
*** notes
generateCaptureId() in data/CaptureId.kt. Format: phone-YYYYMMDD-HHmmss-xxxx (4 random [a-z0-9] chars). Called in CaptureViewModel.save() before Room insert.
*** evidence
- commit: 19b05a8
- tests: manual — check id column in history; each save produces a unique phone-… id
- datetime: [2026-05-18 Sun 17:00]
** DONE implement basic history screen
*** acceptance
- lists recent captures.
- shows pending/synced/failed status.
- failed row shows last error.
- user can retry failed/pending sync.
*** notes
HistoryScreen.kt observes captureDao.observeAll() as Flow. Status colored green/red/grey. Retry button shown for pending/failed; calls updateStatus(id, "pending", null) so the sync worker picks it up in milestone 3.
*** evidence
- commit: 19b05a8
- tests: manual — save captures, navigate to history, verify status labels and retry button
- datetime: [2026-05-18 Sun 17:00]
* milestone 3: android sync
** DONE implement api client
*** acceptance
- base url configurable in app settings or build config.
- bearer token configurable in app settings or build config.
- client can call `/health`.
- client can post capture payload.
*** notes
SynqApiClient in data/api/. OkHttp with 10s timeouts. checkHealth() does GET /health, returns false on any exception so callers never crash. postCapture() maps 401 → failed, non-2xx → failed, already_seen → AlreadySeen, else → Accepted. Uses org.json for request building, kotlinx-serialization for tag decoding.
*** evidence
- commit: 2a963d9
- tests: manual — sync now button, check history screen status
- datetime: [2026-05-18 Sun 18:00]
** DONE implement manual sync
*** acceptance
- sync now posts all pending captures.
- successful posts mark rows synced.
- already-seen response marks row synced.
- failures retain pending/failed status and last error.
*** notes
syncPending() top-level function in data/sync/SyncService.kt — shared by manual sync and WorkManager. CaptureViewModel.syncNow() calls it on Dispatchers.IO, guards with isSyncing flag. Sync icon in top bar shows CircularProgressIndicator while running.
*** evidence
- commit: 2a963d9
- tests: manual — tap sync, verify captures move to synced in history; test with wrong token, verify failed + error message
- datetime: [2026-05-18 Sun 18:00]
** DONE implement opportunistic workmanager sync
*** acceptance
- periodic sync is registered.
- sync only runs when network is available.
- worker first checks `/health`.
- worker does not block capture speed.
*** notes
SyncWorker (CoroutineWorker) in data/sync/. Registered in SynqApp.onCreate() as KEEP unique periodic work (15-min interval, NETWORK_CONNECTED constraint). Health check before sync — exits quietly if server unreachable. Does not touch UI thread.
*** evidence
- commit: 2a963d9
- tests: manual — let app sit on WiFi, verify pending captures eventually sync without manual trigger
- datetime: [2026-05-18 Sun 18:00]
** DONE add simple server reachability settings
*** acceptance
- default url can be set to `http://jeeves.mother:8765`.
- user can change server url.
- user can change token.
- invalid settings do not crash app.
*** notes
SettingsScreen + SettingsViewModel from milestone 2. DataStore-backed. Default serverUrl is http://jeeves.mother:8765. All network calls wrap exceptions so invalid URL/token never crashes — they just produce PostResult.Failed with the error message.
*** evidence
- commit: 19b05a8 (screen), 2a963d9 (wired to sync)
- tests: manual — set bad URL, tap sync, verify captures stay pending/failed with error; set correct URL + token, sync succeeds
- datetime: [2026-05-18 Sun 18:00]
* milestone 4: polish and hardening
** TODO make launch path fast
*** acceptance
- app opens directly to capture field.
- no network call blocks launch.
- no history loading blocks text entry.
** TODO normalize tags
*** acceptance
- whitespace-separated and comma-separated tags both work.
- invalid org tag chars are stripped or replaced.
- duplicate tags collapse.
** TODO handle multiline notes well
*** acceptance
- first line can become note heading if body is multiline.
- full body is preserved under heading.
- todo body remains usable as a single todo heading, with extra lines under it if present.
** TODO add basic logs
*** acceptance
- server logs accepted capture id.
- server logs duplicate capture id.
- server logs rejected payload without dumping secrets.
** TODO create backup note
*** acceptance
- readme documents which paths need backup.
- readme documents restore behavior.
* milestone 5: optional v1.5
** TODO add android share target
*** acceptance
- sharing text to app opens prefilled capture.
- user can save as note or todo.
** TODO add home screen quick capture widget
*** acceptance
- widget opens capture screen quickly.
- does not require sync to work.
** TODO add recent tag chips
*** acceptance
- app shows recent tags.
- tapping chip adds/removes tag.
** TODO add edit-before-sync
*** acceptance
- pending/failed captures can be edited.
- synced captures are read-only unless explicitly duplicated.
* implementation notes for coding agents
- keep the server small and testable.
- keep org formatting in one pure function.
- do not add auth flows, accounts, or public hosting.
- do not add org parsing.
- do not write to org files from android.
- prefer boring explicit settings over clever discovery.
- if a design choice risks sync conflicts, do the simpler append-only thing.