#+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: ", 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 ) - 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.