2e7db21b218b6c4f4857245a40a14baa964fa8e2
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
75928c7afd |
sync: shared notes in the feed, revocations, and text pushes from an edit share
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 2s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Python tests (push) Successful in 15s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 54s
The change feed answers `?shares=1` with every note the caller can see, each saying how it is held, plus a `revoked` list of notes that left them. Granting a share moves the note past the recipient's cursor; ending one, or the owner deleting the note, leaves a revocation on the same cursor. A recipient at edit may push the note's text, and nothing else. Protocol 6, feature `shares`; opt-in, so the floor stays at 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2b2ceaa82e |
attachments sync: attach offline, upload when linked, removals stick (#5168)
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 35s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 9s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Failing after 2m23s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m17s
Desktop could not create an attachment at all, and a removed attachment or dismissed preview came back on the next pull. Now: - core: add_attachment keeps the bytes in the blob store and queues the row (schema v10: attachments.uploaded / upload_error). Push uploads it once its note has landed. A refusal that retrying won't fix (too large, id clash, hash mismatch) is recorded on the file and not re-sent every cycle; the editor shows it. - core: removing a synced attachment or dismissing a preview leaves a tombstone in pending_deletes; push sends it as an `attachment`/`preview` delete, and a pull while it waits doesn't put the row back. A pull also keeps files still waiting to upload instead of replacing them wholesale. - server: PUT /api/sync/attachments/<id> (raw body, sha256-checked, idempotent, size-capped) and child deletes in push, which apply regardless of LWW and answer noop for rows the caller can't see. One store_attachment helper for the upload route, the importer and sync. Protocol 5, feature attachment_sync; the client sends neither to a server without it. - server: migration 0031 makes a link preview's insert/delete bump its note, so background-fetched previews and web dismissals reach linked devices. - desktop: Attach and paste-image work offline (raw-bytes IPC command). - SVG is served as a download by the desktop blob scheme too (as #1981 did for the web), and drawn as a file chip on both. - autosync: drop the catch_unwind; release builds abort on panic, so it only ever worked in debug builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a706644455 |
rename: the server is Inkwell — package, env vars, image, compose, export marker
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 7s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python tests (push) Successful in 15s
CI & Build / integration (push) Successful in 45s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 4m17s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 7m33s
Desktop (Tauri) / Update manifest (push) Successful in 7s
Android / Kotlin + Rust (APK) (push) Successful in 11m25s
Step 2 of milestone 481. The operator chose a full rename (Scribe note 5071), so this goes past the display strings into the identities: - src/thoughtsync → src/inkwell; every import, the Dockerfile and both compose commands, alembic env, pyproject - THOUGHTSYNC_* → INKWELL_* (database URL, secret key, log level, tag/port/bind) - container data dir /var/thoughtsync → /var/inkwell - image git.fabledsword.com/bvandeusen/inkwell; Postgres user/db default inkwell; CI's integration service follows - the files the image serves are inkwell.*. fetch-clients.sh still fetches the thoughtsync-named release assets, because the lanes that publish them are renamed in steps 3 and 4 - exports are written with app "inkwell" Two deliberate exceptions, both because data rides on them: - compose volumes are now named explicitly and overridable (INKWELL_DB_VOLUME, INKWELL_DATA_VOLUME), so a deployment installed as ThoughtSync points at the volumes and DB identity it already has. .env.example says exactly what to set - import still accepts app "thoughtsync", because exports written before the rename are backups. Tested both ways Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fbbe877c46 |
M10.6: client↔server sync protocol handshake (task 1995)
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 42s
CI & Build / Build & push image (push) Successful in 36s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 1m34s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python tests (push) Successful in 14s
Version the sync WIRE PROTOCOL separately from either program's release
version, so a self-hosted server and the desktop app can sit on different
releases and still work out whether they can talk.
Each side declares two numbers — what it speaks, and the oldest counterpart
it accepts. Either side can therefore mark a change breaking without the
other shipping in step, which is the whole point: no app↔server lockstep.
Server advertises on the existing public /api/config (a client must be able
to ask "can I talk to you?" before it holds a device token, or even has an
account): sync_protocol_version, min_client_protocol_version, sync_features.
sync_features exists because a version number can only say newer/older. An
ADDITIVE change earns a capability name instead of a minimum bump, so a
newer client meeting an older server drops that one feature and syncs the
rest, rather than refusing. Raising a minimum is reserved for genuinely
breaking changes — it's the switch that hard-blocks the other side.
Client half is pure decision logic (sync/compat.rs), no I/O, so every branch
is unit-testable — there's no live-server lane in CI. Three outcomes: ok /
degraded{unavailable} / incompatible{reason, client_must_update}. The last
names which side can fix it, so the message is actionable. A server that
predates the handshake sends no protocol fields at all; that reads as
"update the server", deliberately not as a parse error, which would look to
the user like they mistyped the URL.
normalize_base_url defaults a bare host to https://, never http:// —
silently downgrading would put a long-lived device token on the wire in
cleartext because someone omitted five characters. Plain HTTP on a trusted
LAN stays supported; the user types http:// and thereby chooses it.
Transport (the actual fetch) lands next, separately: it needs an HTTP/TLS
stack, and that's a real risk to the Windows cross-compile lane, so it gets
its own CI run to bisect against rather than riding along with this.
No UI here by design — the link/settings surface it feeds is M10.7's, per
this task's own sequencing.
Policy documented in docs/sync.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
|
||
|
|
be34fe8619 |
M9 S4: sync adopts serialization/parse_dt toolkit + normalizes push oracle
DRY: - serialize.py: serialize_label_sync(label) = base serialize_label + the delta-only fields (sync_revision/purged_at/created_at via iso()). sync's changes() adopts it; the local _serialize_label_row near-dup is gone. - sync adopts common.parse_dt (drops the byte-identical _parse_client_dt; 4 call sites) and common.iso for the note delta augmentation. (Manual-label reconciliation was already shared in S3.) Fully folding the note re-augmentation into the serializer waits on the notes.py split. - test_sync: drops the now-redundant _parse_client_dt test (parse_dt is covered in test_notes) + its dead import. Security (issue — push existence-oracle): a foreign-owned id on push was rejected with "not yours", distinguishing "another user's note" from a free id. A legit client only pushes ids of notes it created, so that branch is only hit by a probe (or ~0-prob UUID collision) — now a GENERIC "cannot apply" rejection that doesn't confirm the id exists. The residual create-vs-reject status difference is inherent to client-chosen ids over a global PK and is practically unexploitable (a shared note already exposes its id to recipients). Sync behavior operator-verified on deploy (no Postgres CI lane). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |
||
|
|
68abaa0f3f |
Sync 4: push endpoint POST /api/sync/push (LWW + history snapshot)
The core conflict-resolution step. Applies a batch of client changes,
additive + owner-scoped, with last-write-wins by client edit-time — and a
version-history snapshot on every overwrite so nothing is ever lost.
client_wins(client_edited_at, server_edited_at): apply iff client >= server;
a missing client time never overwrites a real server edit; a missing server
time (new row) yields. Notes compare against updated_at; labels gain an
updated_at (migration 0017, backfilled from created_at) as their LWW field.
Notes:
- upsert with a client-supplied id: create if absent, else LWW-apply the
full note state (title/body/color/kind/pins/trash/remind/position/items/
manual label_ids) with the same ripple as a web edit — derive_display_title,
_rewrite_links, _reconcile_tags (#tags), _rename_inbound_links. Overwriting
an existing title/body snapshots the old version into note_revisions first.
A resurrected tombstone clears purged_at.
- delete: purge tombstone (drop children + attachment files, clear content,
set purged_at), LWW-guarded so a newer server edit survives a stale delete.
Labels: upsert (create/rename/recolor) + delete (detach from notes, tombstone),
LWW-guarded; per-owner name-uniqueness clash on a different id is rejected
rather than raising.
Response: per-item {status: created|applied|kept|noop|rejected, sync_revision};
the client pulls afterward to converge. Whole-note semantics (client sends the
full state, not a partial patch).
Tests (DB-free): client_wins across all edit-time combinations; _parse_client_dt;
push auth-guard. Apply behavior + triggers operator-verified on deploy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
|
||
|
|
8e40ea1188 |
Sync 3: pull endpoint GET /api/sync/changes (M8)
Delta pull for native clients: returns every note + label the caller owns whose sync_revision advanced past ?since=<cursor>, ascending by revision, paginated (?limit, default 500 / max 1000), with the next cursor + has_more. since=0 is a full initial sync. Web app unaffected (new blueprint). Notes and labels share one revision sequence, so the cursor is a single watermark. _page_cursor() handles the two-stream paging: when either stream fills its page, it advances only to the SMALLER of the two page boundaries so nothing between the cursor and the next pull is skipped. Notes reuse _serialize_notes (items/labels/attachments inline) + sync_revision + purged_at (tombstone); labels carry name/color/purged_at/sync_revision. Returns ALL of the owner's notes regardless of state (active/archived/ trash/purged) — a client mirrors everything. Registered sync blueprint. Tests (DB-free): changes auth-guard; _parse_since / _clamp_limit validation; _page_cursor across empty / drained / one-full / both-full. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm |