Replace unused vector clocks with a conflict check that works, drop the rest
The vector clocks in the server were never reached: no client sent one, so the branch was dead and every write was a blind overwrite. Not "the newer edit wins" — "the request that arrives last wins", which is worse: a phone that was offline all day buries the laptop's newer work simply by reconnecting late. Each write now carries baseUpdatedAt, the revision it was written on top of. If the note has moved on since, the incoming edit still wins — a rejection would throw away what was just typed — but the state it replaces is kept as a version marked "conflict", so both exist and the history is where you choose. Clocks could have told a real conflict from a device catching up, but that precision only pays off with a merge interface, and this reuses the version history already built. adoptRevision() stores the revision the server reports back, or the second save in a row would be flagged as conflicting with the first. Also removed, as agreed: - POST /api/notes/pdf — accepted PDFs that nothing could ever read back, with no retrieval route and no viewer. The readme advertised "PDF Import & Annotation"; that claim is gone too. - POST /api/sync — a batch endpoint the client never called. - the tags column — notebooks, starring and search already cover it. Columns on existing databases are left in place. Dropping them buys nothing and is not worth touching a live database for. Tables now have a right-click menu (editor/tableMenu.js) for rows, columns, header and merging. It needs stopPropagation: without it the same event reaches window, where the menu that is mounting reads it as a click elsewhere and closes instantly — the reason the other context menus in App.jsx already do this. 92 browser checks across seven suites, 125 unit tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8c36c6121b
commit
df279a5d36
10 files changed
+367
-152
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# ✏️ InkNote
|
||||
|
||||
A cross-platform note-taking application with rich text editing, freehand drawing, PDF annotation, and encrypted sync — inspired by Samsung Notes.
|
||||
A self-hosted note-taking application with rich text editing, freehand drawing, full-text search and encrypted sync — inspired by Samsung Notes.
|
||||
|
||||
---
|
||||
|
||||
@@ -15,7 +15,6 @@ A cross-platform note-taking application with rich text editing, freehand drawin
|
||||
- **Image Attachments** — Pasted images are stored as deduplicated blobs, not inlined into the note
|
||||
- **Freehand Drawing** — Pen, highlighter, eraser with adjustable thickness and color palette (20 colors, 7 pen sizes)
|
||||
- **Undo/Redo** — Full history stack for drawing operations
|
||||
- **PDF Import & Annotation** — Import PDFs, overlay drawing/highlighting, persistent annotations
|
||||
- **Notebooks & Folders** — Create notebooks with custom gradient covers, organize notes by folder
|
||||
- **Star/Favorite** — Quick access to important notes
|
||||
- **Export** — Any note, notebook or the whole account as Markdown or PDF, entirely in the browser
|
||||
@@ -23,7 +22,7 @@ A cross-platform note-taking application with rich text editing, freehand drawin
|
||||
|
||||
### Sync & Storage
|
||||
- **Offline-First** — Works fully offline, syncs when connection returns
|
||||
- **Conflict-Aware Sync** — Vector clock–based conflict detection; notifies user of merge conflicts
|
||||
- **Conflict-Aware Sync** — A write from a device that missed another's changes keeps the state it replaces as a version, rather than burying it
|
||||
- **WebSocket Real-Time Push** — Changes appear on other devices instantly
|
||||
- **Encrypted at Rest** — AES-256-CBC encryption for all note content on the server
|
||||
- **JWT Authentication** — Username + password accounts with bcrypt password hashing. No public
|
||||
@@ -84,7 +83,7 @@ host up:
|
||||
```
|
||||
$INKNOTE_DATA_DIR/
|
||||
├── db/ # inknote.db — notes, drawings, version history
|
||||
└── uploads/ # legacy raster drawings, PDFs
|
||||
└── uploads/ # image attachments, legacy raster drawings
|
||||
```
|
||||
|
||||
Default is `./data` for development and `/srv/inknote` in production; override
|
||||
@@ -234,13 +233,11 @@ deleted person's session stops working immediately: tokens are signed rather tha
|
||||
| GET | `/api/links` | Every link, for backlink counts |
|
||||
| DELETE | `/api/notes/:id` | Soft-delete note |
|
||||
| PUT | `/api/notes/:id/drawing` | Save vector strokes |
|
||||
| POST | `/api/notes/pdf` | Upload PDF (multipart/form) |
|
||||
|
||||
### Sync
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|------------- |------------------------------------------|
|
||||
| POST | `/api/sync` | Bidirectional sync with conflict reports |
|
||||
|
||||
### WebSocket
|
||||
|
||||
@@ -328,10 +325,11 @@ docker compose -f docker-compose.dev.yml up -d --build # must be running
|
||||
| Suite | Checks | What it holds down |
|
||||
| --- | --- | --- |
|
||||
| `admin` | 15 | no sign-up anywhere; `/api/admin/*` refused for non-admins even when called directly |
|
||||
| `editor` | 12 | tables, task lists, note links, backlinks — and that each survives a reload |
|
||||
| `editor` | 17 | tables and their right-click menu, task lists, note links, backlinks — and that each survives a reload |
|
||||
| `search-attachments` | 12 | body-only matches, ranking, umlauts; images as blob URLs, not inline bytes |
|
||||
| `history` | 10 | a large deletion is snapshotted, restoring works, restoring is itself undoable |
|
||||
| `drawing` | 7 | strokes survive the poller, and match across four viewport widths |
|
||||
| `conflict` | 10 | a late write wins but the state it buries survives as a version |
|
||||
| `export` | 21 | the downloaded bytes are real Markdown, ZIP and PNG, unpacked and read back |
|
||||
|
||||
Each suite creates a throwaway account through the admin API and deletes it
|
||||
@@ -550,13 +548,29 @@ underneath any newer strokes so nothing is lost.
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
InkNote uses **vector clocks** to detect conflicts:
|
||||
Two devices can edit the same note without knowing about each other — a phone
|
||||
that was offline all day, a laptop that was not. When the phone reconnects, its
|
||||
save arrives last, and without care it would simply bury the newer work.
|
||||
|
||||
1. Each device maintains a logical clock counter
|
||||
2. On save, the device increments its counter and includes the full vector clock
|
||||
3. The server compares clocks — if the client clock is strictly newer, the update applies
|
||||
4. If clocks are concurrent (both have changes the other doesn't), a **409 Conflict** is returned
|
||||
5. The client receives both versions and can present a merge UI to the user
|
||||
Each write carries `baseUpdatedAt`: the revision the editing device had when it
|
||||
started. The server compares it with the note's current revision:
|
||||
|
||||
1. **Same** — an ordinary save, nothing to think about.
|
||||
2. **Server is newer** — someone else saved in between. The incoming edit still
|
||||
wins, because rejecting it would throw away whatever was just typed, and on a
|
||||
phone that has been offline for a day that is the worse loss. But the state
|
||||
it replaces is kept as a version marked `conflict`, so both exist, and the
|
||||
history dialog is where you pick which one to keep.
|
||||
|
||||
This replaced vector clocks, which sat in the server unused: no client ever sent
|
||||
one. They can tell a genuine conflict from a device merely catching up, which is
|
||||
strictly more accurate — but that accuracy only pays for itself with a merge
|
||||
interface to spend it on, and comparing revisions needs one field and reuses the
|
||||
version history that already exists.
|
||||
|
||||
**What this does not do:** it will not tell you at the moment it happens. The
|
||||
conflicting version appears in the history, labelled, and that is where you find
|
||||
it. A notification is worth adding if it ever actually bites.
|
||||
|
||||
---
|
||||
|
||||
@@ -566,10 +580,10 @@ InkNote uses **vector clocks** to detect conflicts:
|
||||
Use `note-app.jsx` components with React Native adaptations (replace HTML canvas with `react-native-canvas` or `@shopify/react-native-skia` for drawing). The sync engine works as-is.
|
||||
|
||||
### Flutter
|
||||
Use the sync engine via a JavaScript bridge, or port the API calls to Dart. For drawing, use Flutter's `CustomPainter`. For PDF, use `flutter_pdfview`.
|
||||
Use the sync engine via a JavaScript bridge, or port the API calls to Dart. For drawing, use Flutter's `CustomPainter`.
|
||||
|
||||
### Electron
|
||||
Wrap the React app directly in Electron for desktop. Add native file system access for PDF import/export.
|
||||
Wrap the React app directly in Electron for desktop. Add native file system access for export.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user