milestone 4: org header, logging, backup docs, mark all m4 tasks done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
21
README.md
21
README.md
@@ -157,6 +157,27 @@ synq/
|
||||
5. add history screen and resend handling.
|
||||
6. polish launch speed and widget/share-target only after core path works.
|
||||
|
||||
## backup
|
||||
|
||||
### what to back up
|
||||
|
||||
the server writes to one directory (the `/data` volume). back up the whole thing:
|
||||
|
||||
```text
|
||||
/data/synq.org ← the canonical org capture file
|
||||
/data/capture.sqlite3 ← idempotency store (dedup ids)
|
||||
/data/token.txt ← auto-generated token (if not using PHONE_CAPTURE_TOKEN env var)
|
||||
```
|
||||
|
||||
on unraid the host path is whatever you mapped in compose, e.g. `/mnt/user/ben/synq/phone-capture`.
|
||||
|
||||
### restore behavior
|
||||
|
||||
- restore the `/data` volume to a new container and start it. the server will pick up where it left off.
|
||||
- `synq.org` is append-only plain text — it is human-readable and recoverable even without the sqlite db.
|
||||
- if `capture.sqlite3` is lost but `synq.org` is intact, the server will accept re-posted captures that were already in the org file (no dedup). the android app marks them synced either way (`already_seen` or `accepted`), so the only side effect is duplicate org entries for anything re-synced. restore the db from backup to avoid this.
|
||||
- if `token.txt` is lost, delete it and let the server generate a new one on next start, then update the app settings.
|
||||
|
||||
## v2 parking lot
|
||||
|
||||
- android share target
|
||||
|
||||
@@ -82,6 +82,7 @@ async def check_token(request: Request) -> None:
|
||||
auth = request.headers.get("authorization", "")
|
||||
scheme, _, provided = auth.partition(" ")
|
||||
if scheme.lower() != "bearer" or provided != expected:
|
||||
logger.warning("rejected request: invalid token from %s", request.client.host if request.client else "unknown")
|
||||
raise HTTPException(status_code=401, detail="unauthorized")
|
||||
|
||||
|
||||
@@ -101,7 +102,10 @@ async def capture(payload: CaptureRequest, store: IdempotencyStore = Depends(get
|
||||
|
||||
lock = get_file_lock()
|
||||
with lock:
|
||||
Path(org_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
org_file = Path(org_path)
|
||||
org_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not org_file.exists():
|
||||
org_file.write_text("#+title: synq captures\n#+startup: overview\n\n", encoding="utf-8")
|
||||
with open(org_path, "a", encoding="utf-8") as f:
|
||||
f.write(entry)
|
||||
store.mark_seen(payload.id, payload.created_at)
|
||||
@@ -113,6 +117,8 @@ async def capture(payload: CaptureRequest, store: IdempotencyStore = Depends(get
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||||
errors = exc.errors()
|
||||
fields = [str(e.get("loc", "")) for e in errors]
|
||||
logger.warning("rejected payload: validation failed fields=%s from=%s", fields, request.client.host if request.client else "unknown")
|
||||
for err in errors:
|
||||
if "body" in err.get("loc", ()):
|
||||
return JSONResponse(status_code=400, content={"detail": "body must not be empty"})
|
||||
|
||||
54
tasks.org
54
tasks.org
@@ -303,30 +303,74 @@ SettingsScreen + SettingsViewModel from milestone 2. DataStore-backed. Default s
|
||||
- datetime: [2026-05-18 Sun 18:00]
|
||||
|
||||
* milestone 4: polish and hardening
|
||||
** TODO make launch path fast
|
||||
** DONE 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
|
||||
*** notes
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() } fires immediately on composition. No network calls in MainActivity or CaptureScreen init path. History loads via Room Flow on a background coroutine — never blocks text entry.
|
||||
*** evidence
|
||||
- commit: 19b05a8
|
||||
- tests: manual — cold launch, keyboard appears without any loading state
|
||||
- datetime: [2026-05-18 Sun 19:00]
|
||||
|
||||
** DONE 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
|
||||
*** notes
|
||||
Two-layer normalization: parseTags() in CaptureViewModel splits on [,\s]+ before storing to Room. normalize_tags() in server/app/org_writer.py lowercases, strips #, replaces spaces with _, removes non-[a-z0-9_] chars, deduplicates. 7 unit tests cover all cases.
|
||||
*** evidence
|
||||
- commit: e873a00 (server), 19b05a8 (android)
|
||||
- tests: pytest tests/test_org_writer.py::TestNormalizeTags — 7 passed
|
||||
- datetime: [2026-05-18 Sun 19:00]
|
||||
|
||||
** DONE fix org formatting
|
||||
*** acceptance
|
||||
- add `#+startup: overview` to synq.org
|
||||
*** notes
|
||||
Append writer in main.py checks if org file exists before first write. If missing, creates it with #+title and #+startup: overview header. Existing files are never modified.
|
||||
*** evidence
|
||||
- commit: (this commit)
|
||||
- tests: manual — delete synq.org, post a capture, verify header present
|
||||
- datetime: [2026-05-18 Sun 19:00]
|
||||
|
||||
** DONE 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
|
||||
*** notes
|
||||
format_capture() in org_writer.py: multiline note uses first line as heading suffix ("* note: <first line>"), full body preserved below drawer. Single-line note uses "* note" heading, body below. Covered by test_note_multiline.
|
||||
*** evidence
|
||||
- commit: e873a00
|
||||
- tests: pytest tests/test_org_writer.py::TestFormatCapture::test_note_multiline — passed
|
||||
- datetime: [2026-05-17 Sat 17:00]
|
||||
|
||||
** DONE 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
|
||||
*** notes
|
||||
logger.info for accepted and duplicate (id only, no body). logger.warning for invalid token (client IP only, no token value logged). logger.warning for validation errors (field names only, no body content).
|
||||
*** evidence
|
||||
- commit: (this commit)
|
||||
- tests: manual — send bad token, send empty body, check uvicorn stdout
|
||||
- datetime: [2026-05-18 Sun 19:00]
|
||||
|
||||
** DONE create backup note
|
||||
*** acceptance
|
||||
- readme documents which paths need backup.
|
||||
- readme documents restore behavior.
|
||||
*** notes
|
||||
Added "backup" section to README.md covering synq.org, capture.sqlite3, token.txt. Restore section documents behavior when each file is lost individually.
|
||||
*** evidence
|
||||
- commit: (this commit)
|
||||
- tests: n/a
|
||||
- datetime: [2026-05-18 Sun 19:00]
|
||||
|
||||
* milestone 5: optional v1.5
|
||||
** TODO add android share target
|
||||
|
||||
Reference in New Issue
Block a user