Files
synq/tasks.org
2026-05-18 10:27:34 -04:00

13 KiB

synq tasks

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

TODO 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.

TODO 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.

TODO 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.

TODO 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.

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.