notes: color leaves the model, the wire and all three surfaces
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 19s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m28s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m52s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 4m1s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 19s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m28s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m52s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 4m1s
Step 3 of M315, and the destructive half. Steps 1 and 2 stopped every read of this field: a card is one neutral surface per theme, and the only coloured thing on a board is a tag. What was left was a column written by a picker and read by nothing. Rule 22 — the old path comes out completely. No flag, no fallback, no "override if set". Server: the column, the `?color=` facet, the create/update/serialise paths, the sync assignment, the front-matter line, and Keep's colour map. Alembic 0029 drops it and sweeps `"color"` out of stored saved-filter params — a view that silently filtered on a field the app no longer has would return nothing and never say why. That sweep is Python, not `params::jsonb - 'color'`, because Postgres has no try-cast and one malformed blob would abort a migration that is running over somebody's saved views. `NOTE_COLORS` moves from `models/note.py` to `colors.py`. A palette defined on the model that lost one is an invitation to put the column back; labels still name a colour, so the vocabulary belongs where the normalizer already is. Core: the field, the facet, the `NoteCreateInput`, and every read and write in store/push/pull. Local schema v9 drops the column and does the same saved-filter sweep, guarded on `json_valid` so a corrupt blob loses a key rather than becoming NULL. The uniffi layer drops `NoteEdit::Color` and `NoteDraft.color` with it. Web: `ColorPicker.vue`, the per-card swatch popover and its stylesheet rule, the FilterBar colour row, the facet in the query round-trip, and the colour half of the editor's baseline-and-save. Android: the `ColorSheet`, the `Picker.COLOR` case, the toolbar's swatch dot, `EditorAction.SetColor`. ## The protocol: v4, and the floor deliberately stays at 3 Checked against `compat.rs` and the push handler rather than trusting the `#[serde(default)]` annotation, because the v2 precedent points the other way: v2 dropped `kind` and `title` and DID raise both floors, on the rule that dropping a field a client sends and expects back is breaking. `color` fails the second half of that test. A v3 client reading a v4 note gets `"default"` from its own serde default and draws the colour it derives locally — the board it drew yesterday. A v3 client pushing `color` has the key ignored, since `_assign_note_fields` reads its payload key by key and never validates the shape. Neither direction errors and neither shows anything wrong. `title` was the note's NAME; this is a field that no longer renders. So `SYNC_PROTOCOL_VERSION` and `CLIENT_PROTOCOL_VERSION` go to 4, and both floors stay at 3. `docs/sync.md` carries the reasoning and the per-version history, and its push example is brought back in line — it still listed `title`, `kind` and `items`, all gone before this. Import stays tolerant: a pre-M315 export or a Keep takeout carrying `color:` imports fine, the key simply read past. Old exports must still import. #3041 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+20
-5
@@ -52,6 +52,15 @@ syncs everything else.
|
||||
### The policy
|
||||
|
||||
- **Any wire change** → bump `SYNC_PROTOCOL_VERSION`.
|
||||
- v2 (M13): `kind` and `title` left the wire; **floor raised**, because a v1
|
||||
client kept pushing both and read back notes carrying neither — and `title` was
|
||||
the note's NAME, so an old client showed nameless notes.
|
||||
- v3: attachments/tombstones/revisions.
|
||||
- v4 (M315): `color` left the note; **floor NOT raised**. Both directions degrade
|
||||
in silence and neither loses anything visible — an old client reading a v4 note
|
||||
falls back to the colour it derives locally, and one pushing `color` has the key
|
||||
ignored. The test is not "did a field leave" but "does either side end up
|
||||
showing something wrong".
|
||||
- **Additive change** (a new field, a new capability) → add a `sync_features`
|
||||
name. Do **not** raise a minimum. Old clients keep working.
|
||||
- **Breaking change only** → raise `MIN_CLIENT_PROTOCOL_VERSION` (or the client's
|
||||
@@ -190,18 +199,24 @@ Body: `{ "changes": [ ... ] }` (max 1000 per batch). Each change:
|
||||
|
||||
```json
|
||||
{ "entity": "note", "id": "<uuid>", "op": "upsert", "edited_at": "<iso8601>",
|
||||
"title": "...", "body": "...", "color": "blue", "kind": "text",
|
||||
"body": "...",
|
||||
"pinned": false, "archived": false, "trashed": false, "remind_at": null,
|
||||
"position": 0, "items": [ {"text": "...", "checked": false} ],
|
||||
"recurrence": null, "position": 0,
|
||||
"label_ids": ["<uuid>", ...], "created_at": "<iso8601, on create>" }
|
||||
```
|
||||
|
||||
- **Client-generated ids.** Notes/labels are UUIDs; the client mints the id when
|
||||
it creates the row offline and sends it here. Create-if-absent, else update.
|
||||
- **Whole-note semantics.** A note upsert carries the client's *full* current
|
||||
state (not a partial patch) — the server overwrites all scalar fields, replaces
|
||||
items, and sets manual label memberships from `label_ids` (tag-sourced labels
|
||||
are re-derived from the body). `#tags` are recomputed server-side.
|
||||
state (not a partial patch) — the server overwrites all scalar fields and sets
|
||||
manual label memberships from `label_ids` (tag-sourced labels are re-derived
|
||||
from the body). `#tags` are recomputed server-side. A checklist is `- [ ] ` lines
|
||||
inside `body` (M304), so there is no separate `items` array.
|
||||
- **Fields a change may still carry, and the server reads past.** `title` and
|
||||
`kind` (removed in v2), `items` (M304) and `color` (v4, M315). The server reads
|
||||
its payload key by key and never validates the shape, which is exactly what lets
|
||||
an older client keep pushing a field this one has stopped storing — see the
|
||||
version policy above for why none of those needed a floor raise on their own.
|
||||
- **`op: "delete"`** purges (tombstones) the row. Trashing is just an upsert with
|
||||
`trashed: true`.
|
||||
- **Labels:** `{entity: "label", op: "upsert"|"delete", id, edited_at, name,
|
||||
|
||||
Reference in New Issue
Block a user