initial commit
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# copy to .env and change this
|
||||||
|
PHONE_CAPTURE_TOKEN=change-me-long-random-token
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
.env
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# android
|
||||||
|
android/.gradle/
|
||||||
|
android/local.properties
|
||||||
|
android/**/build/
|
||||||
|
android/captures/
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# ide
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# os
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
170
README.md
Normal file
170
README.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# synq, a tiny android-to-org capture system.
|
||||||
|
|
||||||
|
open app.
|
||||||
|
type, optionally mark as todo, optionally add tags, save.
|
||||||
|
if home server accessible, post unsynced captures to lan-only service and appended to `phone.org`.
|
||||||
|
|
||||||
|
## non-goals
|
||||||
|
|
||||||
|
- no org parser on android
|
||||||
|
- no agenda on android
|
||||||
|
- no sync provider dependency
|
||||||
|
- no nextcloud file editing
|
||||||
|
- no conflict resolution
|
||||||
|
- no public internet exposure
|
||||||
|
- no ai tagging in v1
|
||||||
|
- no account system in v1
|
||||||
|
|
||||||
|
## architecture
|
||||||
|
|
||||||
|
```text
|
||||||
|
android app
|
||||||
|
kotlin + jetpack compose
|
||||||
|
room sqlite local queue
|
||||||
|
workmanager + manual sync
|
||||||
|
okhttp/retrofit client
|
||||||
|
|
||||||
|
home server
|
||||||
|
docker container on jeeves/unraid
|
||||||
|
python + fastapi
|
||||||
|
sqlite idempotency store
|
||||||
|
append-only org writer
|
||||||
|
|
||||||
|
flow
|
||||||
|
user saves capture locally
|
||||||
|
app marks it pending
|
||||||
|
sync runs when server is reachable
|
||||||
|
app posts pending captures to fastapi
|
||||||
|
server validates, dedupes by id, appends to phone.org
|
||||||
|
app marks capture synced
|
||||||
|
```
|
||||||
|
|
||||||
|
## v1 behavior
|
||||||
|
|
||||||
|
the app launches into a focused text box. the user can type immediately.
|
||||||
|
|
||||||
|
controls:
|
||||||
|
|
||||||
|
- note/todo toggle
|
||||||
|
- optional tags field
|
||||||
|
- save
|
||||||
|
- save and close
|
||||||
|
- sync now
|
||||||
|
- small history view showing pending, synced, and failed entries
|
||||||
|
|
||||||
|
each capture has:
|
||||||
|
|
||||||
|
- stable client-generated id
|
||||||
|
- created timestamp with timezone
|
||||||
|
- kind: `note` or `todo`
|
||||||
|
- body text
|
||||||
|
- tags
|
||||||
|
- device name
|
||||||
|
- sync status
|
||||||
|
- optional last error
|
||||||
|
|
||||||
|
## org output
|
||||||
|
|
||||||
|
todo:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* TODO buy printer paper :home:errands:
|
||||||
|
:PROPERTIES:
|
||||||
|
:CREATED: [2026-05-17 sun 14:31]
|
||||||
|
:SOURCE: android
|
||||||
|
:ID: phone-20260517-143122-a8f2
|
||||||
|
:END:
|
||||||
|
```
|
||||||
|
|
||||||
|
note:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* note :retcon:
|
||||||
|
:PROPERTIES:
|
||||||
|
:CREATED: [2026-05-17 sun 14:33]
|
||||||
|
:SOURCE: android
|
||||||
|
:ID: phone-20260517-143322-b91c
|
||||||
|
:END:
|
||||||
|
|
||||||
|
mobile capture should stay dumb and append-only.
|
||||||
|
```
|
||||||
|
|
||||||
|
## server paths
|
||||||
|
|
||||||
|
default container paths:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data/synq.org
|
||||||
|
/data/capture.sqlite3
|
||||||
|
/data/rejected.log
|
||||||
|
```
|
||||||
|
|
||||||
|
recommended unraid host mapping:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mnt/user/synq/phone-capture:/data
|
||||||
|
```
|
||||||
|
|
||||||
|
or map `/data/synq.org` directly to wherever the real org file lives. prefer a dedicated capture file first; emacs can include it in agenda later.
|
||||||
|
|
||||||
|
## security model
|
||||||
|
|
||||||
|
v1 is lan-only. bind the service to the host lan and do not expose it through swag/cloudflare/public dns.
|
||||||
|
|
||||||
|
minimum useful controls:
|
||||||
|
|
||||||
|
- shared bearer token in app and server env
|
||||||
|
- server only accepts json
|
||||||
|
- server rejects empty body
|
||||||
|
- server dedupes ids
|
||||||
|
- server writes append-only org
|
||||||
|
- server never edits or parses existing org
|
||||||
|
- docker volume is backed up
|
||||||
|
|
||||||
|
## repo layout
|
||||||
|
|
||||||
|
suggested monorepo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
synq/
|
||||||
|
android/
|
||||||
|
app/
|
||||||
|
server/
|
||||||
|
app/
|
||||||
|
main.py
|
||||||
|
models.py
|
||||||
|
org_writer.py
|
||||||
|
store.py
|
||||||
|
tests/
|
||||||
|
Dockerfile
|
||||||
|
pyproject.toml
|
||||||
|
docs/
|
||||||
|
api.md
|
||||||
|
android-notes.md
|
||||||
|
server-notes.md
|
||||||
|
docker-compose.example.yml
|
||||||
|
.env.example
|
||||||
|
tasks.org
|
||||||
|
README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## build order
|
||||||
|
|
||||||
|
1. implement server first using curl tests.
|
||||||
|
2. implement android local capture with room.
|
||||||
|
3. implement manual sync.
|
||||||
|
4. add workmanager opportunistic sync.
|
||||||
|
5. add history screen and resend handling.
|
||||||
|
6. polish launch speed and widget/share-target only after core path works.
|
||||||
|
|
||||||
|
## v2 parking lot
|
||||||
|
|
||||||
|
- android share target
|
||||||
|
- quick settings tile or widget
|
||||||
|
- tag chips from recent tags
|
||||||
|
- configurable default tag
|
||||||
|
- edit unsynced entries only
|
||||||
|
- multi-device capture
|
||||||
|
- wireguard-aware sync
|
||||||
|
- local export/import
|
||||||
|
- optional emacs ingest helpers
|
||||||
31
agents.md
Normal file
31
agents.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# claude-code brief
|
||||||
|
|
||||||
|
build the phone capture project described in this repo.
|
||||||
|
|
||||||
|
priority order:
|
||||||
|
|
||||||
|
1. server mvp
|
||||||
|
2. server tests
|
||||||
|
3. docker packaging
|
||||||
|
4. android local capture
|
||||||
|
5. android manual sync
|
||||||
|
6. android opportunistic sync
|
||||||
|
7. history/settings polish
|
||||||
|
|
||||||
|
hard constraints:
|
||||||
|
|
||||||
|
- do not build mobile org-mode.
|
||||||
|
- do not parse org files on android.
|
||||||
|
- do not write org files from android.
|
||||||
|
- do not add nextcloud or file-sync logic.
|
||||||
|
- do not expose the service publicly.
|
||||||
|
- do not add accounts/oauth.
|
||||||
|
- do not block app launch on network checks.
|
||||||
|
- keep the server append-only and idempotent.
|
||||||
|
- keep org formatting in a pure/tested function.
|
||||||
|
|
||||||
|
start by implementing the fastapi service according to `docs/api.md` and `docs/server-notes.md`. after the server passes tests, create the android app according to `docs/android-notes.md`.
|
||||||
|
|
||||||
|
before adding any v2 features, confirm all acceptance criteria in `tasks.org` milestone 1-3 are done.
|
||||||
|
|
||||||
|
project name is "synq", use this as basic title/prefix as necessary (not "phone-sync")
|
||||||
15
docker-compose.example.yml
Normal file
15
docker-compose.example.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
services:
|
||||||
|
phone-capture:
|
||||||
|
build: ./server
|
||||||
|
container_name: phone-capture
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8765:8765"
|
||||||
|
environment:
|
||||||
|
PHONE_CAPTURE_TOKEN: "${PHONE_CAPTURE_TOKEN}"
|
||||||
|
PHONE_CAPTURE_ORG_PATH: "/data/phone.org"
|
||||||
|
PHONE_CAPTURE_DB_PATH: "/data/capture.sqlite3"
|
||||||
|
PHONE_CAPTURE_HOST: "0.0.0.0"
|
||||||
|
PHONE_CAPTURE_PORT: "8765"
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/org/phone-capture:/data
|
||||||
130
docs/android-notes.md
Normal file
130
docs/android-notes.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# android notes
|
||||||
|
|
||||||
|
## stack
|
||||||
|
|
||||||
|
- kotlin
|
||||||
|
- jetpack compose
|
||||||
|
- room
|
||||||
|
- okhttp or retrofit
|
||||||
|
- workmanager
|
||||||
|
- kotlinx serialization or moshi
|
||||||
|
|
||||||
|
## main screens
|
||||||
|
|
||||||
|
### capture screen
|
||||||
|
|
||||||
|
default screen. should open fast and focus text immediately.
|
||||||
|
|
||||||
|
fields:
|
||||||
|
|
||||||
|
- body text
|
||||||
|
- todo checkbox or segmented note/todo control
|
||||||
|
- tags text field
|
||||||
|
- save
|
||||||
|
- save and close
|
||||||
|
- sync now
|
||||||
|
|
||||||
|
do not run network checks before user can type.
|
||||||
|
|
||||||
|
### history screen
|
||||||
|
|
||||||
|
small list of recent captures.
|
||||||
|
|
||||||
|
show:
|
||||||
|
|
||||||
|
- created timestamp
|
||||||
|
- first line/body preview
|
||||||
|
- kind
|
||||||
|
- tags
|
||||||
|
- status: pending/synced/failed
|
||||||
|
- last error when failed
|
||||||
|
- retry action for pending/failed
|
||||||
|
|
||||||
|
### settings screen
|
||||||
|
|
||||||
|
minimum fields:
|
||||||
|
|
||||||
|
- server base url
|
||||||
|
- bearer token
|
||||||
|
- device label
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
|
||||||
|
```text
|
||||||
|
server base url: http://jeeves.mother:8765
|
||||||
|
device label: android
|
||||||
|
```
|
||||||
|
|
||||||
|
## local database sketch
|
||||||
|
|
||||||
|
capture entity:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class CaptureEntity(
|
||||||
|
val id: String,
|
||||||
|
val createdAt: String,
|
||||||
|
val kind: String,
|
||||||
|
val body: String,
|
||||||
|
val tagsJson: String,
|
||||||
|
val device: String,
|
||||||
|
val status: String,
|
||||||
|
val syncedAt: String?,
|
||||||
|
val lastError: String?
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
status values:
|
||||||
|
|
||||||
|
```text
|
||||||
|
pending
|
||||||
|
syncing
|
||||||
|
synced
|
||||||
|
failed
|
||||||
|
```
|
||||||
|
|
||||||
|
## save behavior
|
||||||
|
|
||||||
|
- trim body.
|
||||||
|
- if empty, do not save.
|
||||||
|
- generate id.
|
||||||
|
- persist row with status pending.
|
||||||
|
- clear input.
|
||||||
|
- optionally close if user used save and close.
|
||||||
|
|
||||||
|
## sync behavior
|
||||||
|
|
||||||
|
manual sync and workmanager should share the same sync service.
|
||||||
|
|
||||||
|
algorithm:
|
||||||
|
|
||||||
|
```text
|
||||||
|
load pending + failed captures
|
||||||
|
for each capture:
|
||||||
|
post to server
|
||||||
|
if accepted or already_seen:
|
||||||
|
mark synced
|
||||||
|
else:
|
||||||
|
mark failed with last_error
|
||||||
|
```
|
||||||
|
|
||||||
|
do not delete synced captures in v1. keep local history.
|
||||||
|
|
||||||
|
## workmanager
|
||||||
|
|
||||||
|
constraints:
|
||||||
|
|
||||||
|
- network available
|
||||||
|
|
||||||
|
periodic behavior:
|
||||||
|
|
||||||
|
- check `/health`
|
||||||
|
- if healthy, sync pending/failed
|
||||||
|
- if not healthy, exit quietly
|
||||||
|
|
||||||
|
do not try to perfectly detect home network in v1. reachability is enough. if it can hit `jeeves.mother`, it is home/vpn enough.
|
||||||
|
|
||||||
|
## launch speed
|
||||||
|
|
||||||
|
hard rule: no network call before text entry is usable.
|
||||||
|
|
||||||
|
load settings and db async. the first rendered thing should be a focused text box.
|
||||||
177
docs/api.md
Normal file
177
docs/api.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# api contract
|
||||||
|
|
||||||
|
base url example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://jeeves.mother:8765
|
||||||
|
```
|
||||||
|
|
||||||
|
all write endpoints require:
|
||||||
|
|
||||||
|
```text
|
||||||
|
authorization: bearer <phone_capture_token>
|
||||||
|
content-type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
## get /health
|
||||||
|
|
||||||
|
request:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
|
||||||
|
response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"service": "synq",
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## post /capture
|
||||||
|
|
||||||
|
request:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /capture
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "phone-20260517-143122-a8f2",
|
||||||
|
"created_at": "2026-05-17T14:31:22-04:00",
|
||||||
|
"kind": "todo",
|
||||||
|
"body": "buy printer paper",
|
||||||
|
"tags": ["home", "errands"],
|
||||||
|
"device": "android"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
field rules:
|
||||||
|
|
||||||
|
- `id`: required, stable, unique per capture.
|
||||||
|
- `created_at`: required iso-8601 datetime with timezone.
|
||||||
|
- `kind`: required, either `note` or `todo`.
|
||||||
|
- `body`: required, trimmed, non-empty.
|
||||||
|
- `tags`: required array, may be empty.
|
||||||
|
- `device`: required string, e.g. `android`.
|
||||||
|
|
||||||
|
success, newly accepted:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"status": "accepted",
|
||||||
|
"id": "phone-20260517-143122-a8f2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
success, duplicate/idempotent retry:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"status": "already_seen",
|
||||||
|
"id": "phone-20260517-143122-a8f2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
invalid token:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "unauthorized"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
invalid payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"detail": "body must not be empty"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## org formatting rules
|
||||||
|
|
||||||
|
todo capture:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* TODO buy printer paper :home:errands:
|
||||||
|
:PROPERTIES:
|
||||||
|
:CREATED: [2026-05-17 sun 14:31]
|
||||||
|
:SOURCE: android
|
||||||
|
:ID: phone-20260517-143122-a8f2
|
||||||
|
:END:
|
||||||
|
```
|
||||||
|
|
||||||
|
note capture:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* note :retcon:
|
||||||
|
:PROPERTIES:
|
||||||
|
:CREATED: [2026-05-17 sun 14:33]
|
||||||
|
:SOURCE: android
|
||||||
|
:ID: phone-20260517-143322-b91c
|
||||||
|
:END:
|
||||||
|
|
||||||
|
mobile capture should stay dumb and append-only.
|
||||||
|
```
|
||||||
|
|
||||||
|
multiline note capture:
|
||||||
|
|
||||||
|
input body:
|
||||||
|
|
||||||
|
```text
|
||||||
|
retcon capture idea
|
||||||
|
|
||||||
|
phone should produce records, not edit org files.
|
||||||
|
```
|
||||||
|
|
||||||
|
output:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* note: retcon capture idea :retcon:
|
||||||
|
:PROPERTIES:
|
||||||
|
:CREATED: [2026-05-17 sun 14:33]
|
||||||
|
:SOURCE: android
|
||||||
|
:ID: phone-20260517-143322-b91c
|
||||||
|
:END:
|
||||||
|
|
||||||
|
retcon capture idea
|
||||||
|
|
||||||
|
phone should produce records, not edit org files.
|
||||||
|
```
|
||||||
|
|
||||||
|
suggested behavior: use first line as heading suffix for notes when helpful, but preserve full body below the drawer.
|
||||||
|
|
||||||
|
## idempotency
|
||||||
|
|
||||||
|
server must never append the same `id` twice.
|
||||||
|
|
||||||
|
recommended implementation:
|
||||||
|
|
||||||
|
1. begin sqlite transaction.
|
||||||
|
2. check whether id already exists.
|
||||||
|
3. if exists, return `already_seen`.
|
||||||
|
4. append org entry.
|
||||||
|
5. insert id into sqlite.
|
||||||
|
6. commit.
|
||||||
|
|
||||||
|
if append succeeds but sqlite insert fails, log loudly. safest implementation may insert first inside a transaction and mark appended after write, but keep the code simple and test the duplicate path.
|
||||||
|
|
||||||
|
## status codes
|
||||||
|
|
||||||
|
- 200: accepted or already seen.
|
||||||
|
- 400: malformed request or invalid capture.
|
||||||
|
- 401: missing/invalid token.
|
||||||
|
- 500: server failed to append or persist state.
|
||||||
|
|
||||||
|
android should treat `accepted` and `already_seen` as synced.
|
||||||
117
docs/server-notes.md
Normal file
117
docs/server-notes.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# server notes
|
||||||
|
|
||||||
|
## stack
|
||||||
|
|
||||||
|
- python
|
||||||
|
- fastapi
|
||||||
|
- pydantic
|
||||||
|
- sqlite
|
||||||
|
- uvicorn
|
||||||
|
- pytest
|
||||||
|
- docker
|
||||||
|
|
||||||
|
## env vars
|
||||||
|
|
||||||
|
```text
|
||||||
|
PHONE_CAPTURE_TOKEN=change-me
|
||||||
|
PHONE_CAPTURE_ORG_PATH=/data/synq.org
|
||||||
|
PHONE_CAPTURE_DB_PATH=/data/capture.sqlite3
|
||||||
|
PHONE_CAPTURE_HOST=0.0.0.0
|
||||||
|
PHONE_CAPTURE_PORT=8765
|
||||||
|
```
|
||||||
|
|
||||||
|
## endpoints
|
||||||
|
|
||||||
|
- `GET /health`
|
||||||
|
- `POST /capture`
|
||||||
|
|
||||||
|
see `docs/api.md`.
|
||||||
|
|
||||||
|
## file writing
|
||||||
|
|
||||||
|
server appends to one org file. it does not parse or rewrite existing org content.
|
||||||
|
|
||||||
|
recommended append behavior:
|
||||||
|
|
||||||
|
- format entry into string ending with newline.
|
||||||
|
- acquire a process-local lock for append.
|
||||||
|
- open file in append mode with utf-8.
|
||||||
|
- write exactly one entry.
|
||||||
|
- flush/close.
|
||||||
|
- store capture id in sqlite.
|
||||||
|
|
||||||
|
## tag normalization
|
||||||
|
|
||||||
|
org heading tags must look like:
|
||||||
|
|
||||||
|
```org
|
||||||
|
:home:errands:
|
||||||
|
```
|
||||||
|
|
||||||
|
normalization:
|
||||||
|
|
||||||
|
- lowercase
|
||||||
|
- trim whitespace
|
||||||
|
- strip leading `#`
|
||||||
|
- replace spaces with `_`
|
||||||
|
- remove chars not matching `[a-z0-9_@#%]` or choose a stricter set
|
||||||
|
- collapse duplicates
|
||||||
|
- omit empty tags
|
||||||
|
|
||||||
|
## heading rules
|
||||||
|
|
||||||
|
todo:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* TODO <first line> :tag:
|
||||||
|
```
|
||||||
|
|
||||||
|
note single-line:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* note: <body> :tag:
|
||||||
|
```
|
||||||
|
|
||||||
|
note multiline:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* note: <first line> :tag:
|
||||||
|
...
|
||||||
|
<body preserved below drawer>
|
||||||
|
```
|
||||||
|
|
||||||
|
## auth
|
||||||
|
|
||||||
|
use one shared bearer token.
|
||||||
|
|
||||||
|
do not add oauth, users, sessions, or accounts.
|
||||||
|
|
||||||
|
## docker
|
||||||
|
|
||||||
|
bind port on lan only. do not publish via swag/cloudflare.
|
||||||
|
|
||||||
|
example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
synq:
|
||||||
|
build: ./server
|
||||||
|
ports:
|
||||||
|
- "8765:8765"
|
||||||
|
environment:
|
||||||
|
PHONE_CAPTURE_TOKEN: "${PHONE_CAPTURE_TOKEN}"
|
||||||
|
PHONE_CAPTURE_ORG_PATH: "/data/synq.org"
|
||||||
|
PHONE_CAPTURE_DB_PATH: "/data/capture.sqlite3"
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/mac/synq/phone-capture:/data
|
||||||
|
```
|
||||||
|
|
||||||
|
## tests to write first
|
||||||
|
|
||||||
|
- org formatter: todo with tags.
|
||||||
|
- org formatter: note with no tags.
|
||||||
|
- org formatter: multiline note.
|
||||||
|
- api: valid post appends once.
|
||||||
|
- api: duplicate post appends once.
|
||||||
|
- api: missing token rejected.
|
||||||
|
- api: empty body rejected.
|
||||||
214
tasks.org
Normal file
214
tasks.org
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
#+title: phone capture 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
|
||||||
|
** TODO create monorepo skeleton
|
||||||
|
*** acceptance
|
||||||
|
- `android/`, `server/`, and `docs/` directories exist.
|
||||||
|
- root `README.md`, `tasks.org`, `.env.example`, and `docker-compose.example.yml` exist.
|
||||||
|
** TODO 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.
|
||||||
|
** TODO document v1 scope in README
|
||||||
|
*** acceptance
|
||||||
|
- states capture-only scope.
|
||||||
|
- states non-goals.
|
||||||
|
- states architecture and build order.
|
||||||
|
|
||||||
|
* milestone 1: server mvp
|
||||||
|
** TODO 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`.
|
||||||
|
** TODO 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.
|
||||||
|
** TODO 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.
|
||||||
|
** TODO implement sqlite idempotency store
|
||||||
|
*** acceptance
|
||||||
|
- accepted capture ids are stored.
|
||||||
|
- repeated id returns accepted/already-seen without appending.
|
||||||
|
- db path is configurable.
|
||||||
|
** TODO 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.
|
||||||
|
** TODO add server tests
|
||||||
|
*** acceptance
|
||||||
|
- health endpoint test.
|
||||||
|
- valid capture append test.
|
||||||
|
- duplicate capture test.
|
||||||
|
- invalid token test.
|
||||||
|
- empty body rejection test.
|
||||||
|
** TODO 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.
|
||||||
|
|
||||||
|
* milestone 2: android local capture
|
||||||
|
** TODO 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`.
|
||||||
|
** TODO 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.
|
||||||
|
** TODO 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.
|
||||||
|
** TODO 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.
|
||||||
|
** TODO implement basic history screen
|
||||||
|
*** acceptance
|
||||||
|
- lists recent captures.
|
||||||
|
- shows pending/synced/failed status.
|
||||||
|
- failed row shows last error.
|
||||||
|
- user can retry failed/pending sync.
|
||||||
|
|
||||||
|
* 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.
|
||||||
Reference in New Issue
Block a user