Add version history with rollback and per-note created/synced info
All checks were successful
ci / test (push) Successful in 3m8s
ci / release (push) Successful in 11m21s

Server (schema v2, auto-migrates): every accepted overwrite snapshots the
superseded revision into note_history (capped at 50 per note); new
GET /api/v1/notes/{id}/history endpoint; compact purges orphaned history.

Client: notes get a synced_at stamp on every confirmed server exchange;
an info footer under the editor and a Ctrl/Cmd+I panel show created/
modified/last-synced plus the revision list. Restoring a revision applies
it as a normal edit through the sync path, so rollback is non-destructive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 22:20:19 +02:00
parent bd2b8e6241
commit c389bab46e
12 changed files with 563 additions and 9 deletions

14
SPEC.md
View File

@@ -92,17 +92,29 @@ CREATE TABLE notes (
CREATE INDEX idx_notes_version ON notes(version);
CREATE TABLE meta (k TEXT PRIMARY KEY, v TEXT); -- schema_version, next_version counter
CREATE TABLE note_history ( -- schema v2: version history
note_id TEXT NOT NULL,
version INTEGER NOT NULL, -- version of the superseded revision
content TEXT NOT NULL DEFAULT '',
tags TEXT NOT NULL DEFAULT '[]',
modified_at INTEGER NOT NULL,
replaced_at INTEGER NOT NULL, -- unix ms when it was overwritten
PRIMARY KEY (note_id, version)
);
```
- **Title is derived**, never stored: first non-empty line of `content`, markdown heading markers stripped for display.
- `version` is a single global monotonically increasing counter (like a Lamport clock per server). Every accepted write bumps the global counter and stamps the note. This makes "give me everything changed since cursor X" trivial.
- Tombstones are kept forever (personal scale; millions of notes are not expected). A `tefterd compact` subcommand may purge tombstones older than N days.
- **Version history:** whenever an accepted push overwrites an existing note (fast-forward or edit-over-tombstone), the superseded state is copied into `note_history`, capped at the newest 50 revisions per note. Conflict pushes touch nothing, so they record nothing. `compact` purges history rows of notes that no longer exist. Rollback is client-side: fetch a revision, apply its content as a normal edit.
### Client store (IndexedDB)
Object store `notes`: same fields as server, plus:
- `dirty: boolean` — locally modified, not yet pushed.
- `baseVersion: number` — server version this local copy was derived from (0 for never-synced).
- `synced_at?: number` — unix ms of the last confirmed exchange with the server for this note (accepted push or applied pull); absent = never synced.
Object store `meta`: `cursor` (last server version pulled), `serverUrl`, `token`.
@@ -116,6 +128,7 @@ Design: **pull-then-push, last-write-wins with conflict copies** (Simplenote-sty
|---|---|---|
| GET | `/changes?since=<cursor>&limit=500` | Pull notes with `version > cursor`, ordered by version. Returns `{notes: [...], cursor: <max version returned>, more: bool}` |
| POST | `/notes/batch` | Push local changes. Body: `{notes: [{id, content, tags, created_at, modified_at, deleted, baseVersion}]}` |
| GET | `/notes/{id}/history` | Superseded revisions of a note, newest first: `{revisions: [{note_id, version, content, tags, modified_at, replaced_at}]}` (empty list for unknown ids) |
| GET | `/health` | Liveness + schema version |
| POST | `/import/simplenote` | Multipart upload of Simplenote export zip (also available as CLI) |
@@ -187,6 +200,7 @@ This section is the heart of the product. The NV model must be reproduced exactl
| Cmd/Ctrl+Shift+P | Toggle markdown preview for current note |
| Cmd/Ctrl+K | Cycle tag filter (simple tag dropdown) |
| Cmd/Ctrl+J / Cmd/Ctrl+Shift+J | Next / previous note in list while in editor |
| Cmd/Ctrl+I | Note info (created / modified / last synced) + version history panel; a revision can be previewed and restored (restore = normal edit, non-destructive) |
### Editor
- CodeMirror 6, markdown mode, light syntax styling only (bold headings, dim syntax marks). Monospace or user-set font. No WYSIWYG.