c28f2bc00e367a22fe8e47eaf8b0abed1adb8083
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7a77a0e1b9 |
desktop: fix a retention test that raced the wall clock
`a_note_exactly_at_the_boundary_survives` stamped a note 30 days ago and then asked the sweep — which reads `now` microseconds later — whether it was strictly older than 30 days. It was, by those microseconds. The assertion was wrong, not the code: an exact tie isn't observable against a wall clock. Now stamps a note with a minute of its window still to run, which is the property actually worth pinning: the comparison is strictly-older, so a note inside the window is kept. Also rewrote the row scan as plain statements. The `filter_map` over `query_map` swallowed real rusqlite errors through `.ok()?` on the way to skipping unparseable timestamps — the two cases deserve different treatment, and only the second should be silent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi |
||
|
|
e64d67e904 |
Expire trash after 30 days, and make the deadline something you can see
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 12s
CI & Build / Build & push image (push) Successful in 44s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 1m45s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m12s
Trash had no end. A note sat in /trash until someone emptied it by hand, and its attachment BYTES sat on disk the whole time — the pile-up the operator asked about. Nothing purged; there was no scheduler at all. Retention is server-owned: `trash_retention_days` (default 30, 0 = keep forever) in the settings registry, so it lands in admin Settings with no migration and takes effect without a restart. A background sweep started in before_serving does the work. Clients learn about a purge the way they learn about any deletion — as a tombstone on the delta feed. An auto-purge nobody can see coming is data loss on a timer, so the window is now visible: /api/config publishes it, notes carry `deleted_at`, Trash leads with the policy, and each card counts down. The countdown rounds DOWN — saying "1 day left" for a note with ten minutes on the clock is the one error here that actually costs someone a note. Three things this turned up on the way: - `DELETE /api/notes/<id>` hard-deleted the row, leaving no tombstone at all. A permanent delete in the web UI never reached a linked device, which would keep its copy forever and push it back on the next edit. It now purges through the same path as everything else. - The purge left `note_revisions` and `note_link_previews` behind. A revision holds the full body, so the text of a "permanently deleted" note was still sitting in the database. - `deleted_at` now SURVIVES a purge instead of being cleared. It's still true, and it means every query that says "not trashed" excludes tombstones for free — without it a content-less row reads as a perfectly normal active note and shows up on the board as a blank card. Desktop keeps its own clock only when there's nobody else to keep one: the sweep runs at startup on an UNLINKED device and refuses otherwise. A linked client that expired notes on its own schedule could destroy something the server was deliberately keeping, then push that delete upstream. Local policy must never outrank the server's — so it also adopts the server's window for the countdown rather than showing its offline default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi |
||
|
|
ed623a7bef |
M10.7d: download attachment bytes into a content-addressed store (task 2107)
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 35s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 1m29s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 1m47s
The client half of task 1942's server work. Metadata already rides the delta feed; this fetches the payload so a synced image exists on the device. Blobs are filed under their own sha256, so the same image attached to five notes is stored once and re-downloading it is free — the dedupe the task asks for falls out of content addressing rather than needing bookkeeping. The hash is also the integrity check, applied on the way IN. Bytes that don't hash to what the server advertised are refused rather than filed under a name that lies about them — and because the blob then still counts as missing, the next sync simply tries again. SECURITY: the hash arrives in a server response and becomes a FILENAME, so it is validated as 64 hex characters before touching the filesystem. Without that, a hostile or buggy server could send "../../..." and steer a write outside the blob directory. Tested. A failed attachment never fails the sync. Notes are the primary data and have already landed; aborting here would let one unreachable file block every future sync. Counted, logged, surfaced in the UI as "they'll retry on the next sync", and retried because the blob is still absent. sha2 is pure Rust, so the Windows cross-compile lane pays nothing for it — the constraint recorded in ci-requirements.md. SPLIT, deliberately: this stores the bytes but does NOT yet render them in the webview. That half needs a custom URI scheme or the asset protocol, whose URL form differs by platform (Windows uses http://scheme.localhost/, others scheme://localhost/) — and CI cannot verify webview rendering at all, being headless with no webview. Guessing at it here would ship an unverifiable change on the most fragile lane. Follow-up filed; synced images will show as broken until it lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi |
||
|
|
2e32ecda6e |
desktop: rustfmt the pull tests; run fmt after clippy/test
Two macro-argument splits and a stray blank line, applied verbatim from run 2900's cargo fmt --check diff. Also reorders the Linux job so `cargo fmt --check` runs AFTER clippy and the tests. Fail-fast ordering would normally put the cheapest check first, but there is no Rust toolchain on the workstation, so this lane is verified entirely in CI — and a formatting nit failing first SKIPS clippy and the tests, making a whole cycle teach nothing but whitespace. That has now cost four cycles in this session alone. It still runs before the 20-40 minute bundle build, so a fmt failure doesn't burn that either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi |
||
|
|
dc8b2d360d |
M10.7b: pull the change feed into the local store (task 2105)
Server -> local. sync/wire.rs mirrors the delta-feed JSON exactly as notes/serialize.py sends it; sync/pull.rs applies it. ATOMICITY IS THE POINT. The cursor is written in the SAME transaction as the page it describes. A cursor committed ahead of its data would skip those rows forever while reporting a clean sync — the worst kind of failure, because nothing looks wrong. A test forces a mid-page failure and asserts the cursor stayed put. Every degradation leans toward re-downloading rather than skipping: an unparseable cursor means full sync, wire fields are all defaulted so a newer server adding a field (or an older one omitting one) yields a partial note instead of a rejected page, and a page that fails rolls back whole. Labels are applied before notes so a membership never references a row that doesn't exist. A note also carries enough of its labels to materialize them, because notes and labels page from ONE shared sequence and a note can arrive referencing a label whose own delta landed in an earlier page. via_tag is applied verbatim rather than re-deriving #tags from the body. The server already reconciled them on save, and re-deriving would go through the local find-or-create path, which marks new labels dirty — pushing them straight back. Sync churn manufactured out of nothing. Duplicate-label merge, the subtle one: a label created offline can collide by name with one the server already had under a different id. Both sides enforce one label per name, so the server's row has to win — but simply deleting the local duplicate would CASCADE its note_labels away, stripping the label off notes this pull never mentions, with no later page to repair it. So we free the name, insert the server's row, re-point the memberships, then drop the husk. Tested. Children (items/attachments/previews/labels) are replaced wholesale rather than diffed: a delta carries the note's FULL state, so what arrived IS the complete set, and diffing could strand a row the server no longer has. The loop trusts the data over the flag — a server claiming has_more without advancing its cursor stops with an error instead of spinning forever. Pull can overwrite a row with unpushed local edits. The documented cycle is push-then-pull (M10.7c), so that should never happen; when it does it's counted as clobbered_dirty and logged rather than hidden. 17 tests, all against an in-memory database. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi |