initial commit
This commit is contained in:
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.
|
||||
Reference in New Issue
Block a user