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
There was essentially no logging — useless for proving the app renders across
different environments. Add real observability:
- tauri-plugin-log -> stdout (so `2>&1 | tee` captures a run) AND a persistent
file in the app log dir (grabbable after the fact on any machine). Level Info.
- Startup diagnostics: app version, OS/arch, the Linux display/session stack
(XDG_SESSION_TYPE, desktop, Wayland/X11, GDK_BACKEND), the WebKit render-
hardening vars actually in effect, resolved log + data dirs, DB open/migrate
result, and note/label counts.
- log_event command + a frontend logEvent() helper: boot line (data source +
WebKit user-agent) from main.ts, first-route config/session/destination from
the router guard, and — via the bridge invoke() wrapper — every failed Tauri
command named with its error, so a broken basic function is self-identifying.
Task 2040.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
The on-device core that makes the desktop app work with no server and no login.
- rusqlite (bundled SQLite, so no system libsqlite dependency to vary across
builds); uuid v4 ids; RFC3339/Date.toISOString-compatible timestamps.
- Schema mirroring the note model: notes, labels, note_labels (with via_tag),
checklist_items, attachments, link_previews, note_revisions, saved_filters,
plus per-row sync_revision/dirty + a sync_state row for the M10.7 engine.
user_version-gated migrations.
- derive.rs: pure [[wiki-link]] + #tag scanners (mirror the frontend inline
rules, no regex dep) with unit tests; #tags re-sync via_tag labels on save,
[[links]] drive backlinks at query time (derived, never stored).
- store.rs: the full repository surface (facet/label/date/text list, create,
PATCH-semantics update, pin/archive/color/kind, checklist items, labels CRUD
+ merge, reminders complete/snooze, reorder, trash/restore/delete, revisions
+ restore, titles/search/backlinks/link-search, saved filters).
- commands.rs: ~38 #[tauri::command]s over a Mutex<Connection> in managed state.
- lib.rs: opens the DB in the platform app-data dir on setup; synthetic offline
config/user so the auth-gated router resolves with no login.
Attachment upload / URL unfurl / import are intentionally deferred (network/file
concerns); adapters/local.ts (M10.5) wires all of the above via invoke.
Task 1993.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm