initial commit

This commit is contained in:
2026-05-17 15:59:43 -04:00
commit c08b3fe80d
9 changed files with 883 additions and 0 deletions

130
docs/android-notes.md Normal file
View 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
View 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
View 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.