d68b0775c2612272fd996d19cf5dfed257c8f0ae
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1757ec9af3 |
Chapters: folders inside a notebook
A note is in exactly one chapter, or in none — which means the notebook itself. Chapters sit above the loose notes; tapping one opens its own list and the menu button becomes the way back out, which on a phone is the only way, since the list fills the screen. A chapter is not a wastebasket. Removing one never removes what it held: the notes come back to the notebook, where they can be seen and filed again. That is ON DELETE SET NULL, and it is also written out in the delete route, because it is the promise the whole thing rests on and should be visible where it happens rather than inferred from a schema. The dialog says so too, with the number of notes affected, and the suite removes a chapter holding three notes and asks the server for all three. Three rules keep the two levels honest. A note filed into a chapter leaves the notebook's own list, or filing it would appear to do nothing. A chapter belongs to its notebook, so the server refuses to file a note into a chapter of a book it is not in, and moving a note to another notebook clears its chapter. And separators belong to the list they were drawn in: a separator's order counts the notes before it, which means nothing across lists. Two bugs found while building it, both a day old and both affecting the desktop rather than the phone they were written for: - A submenu was clipped away. The context menu had been given `overflow-y: auto`, which cuts off anything flying out sideways — so the notebooks under "Move to..." were invisible and every click on them landed on the note list behind. The side was inverted too: `right: 100%` places a panel on the left. - A separator was a one-pixel hairline in the border colour with faint grey text on near-black. Present, and on a phone effectively invisible. And one test that never measured what it claimed. It asked whether a dragged note had become "the most recently changed" by reading the order on screen — but three notes made seconds apart share an `updated_at` second, so that order is a tie broken arbitrarily. It passed on luck for weeks and fell over on a run a fraction faster. It now compares the timestamps themselves, before and after the drag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
608da16ac2 |
Make the phone work: touch reordering, reachable menus, and nothing lost offline
Four things that were built for a mouse on a desk and quietly did nothing useful on a phone. The last one is the serious one. **A note made offline lost everything written into it.** Its create reached the server, the server named it something else, and every edit still queued behind it went on addressing the name only that device ever knew — 404, five retries, thrown away. The note arrived empty and untitled, with nothing to say that anything had been lost. The database was repointed at the new id all along; the queue was not. It is now, by replacing the id across the whole queued payload rather than in a list of named fields, because a list of fields is a list to forget to add to. The same held for a notebook made offline and everything filed into it. **The order of notes could not be changed on a phone**, because the list went through the HTML5 drag API, which mobile browsers never fire from touch. Pointer events instead, which mouse, pen and finger all produce. A mouse still drags a row from anywhere; a finger drags from a grip, because pressing the row has to go on scrolling and holding it has to go on opening the menu. The row under the finger is found with elementFromPoint — a finger raises no pointerover on anything it did not press. **"Move to..." opened off the side of the screen**, and could not be opened at all without a hover. It is a tap now, unfolding in place where there is no room beside the menu and flipping sides where there is. The menu itself is placed by measuring rather than by assuming it is 220px wide, its rows are tall enough to hit with a thumb, and Escape closes it. **Installing said nothing when it could not be done.** The entry appeared only when the browser had offered a prompt, so where installing was impossible there was no button and no reason. It is always there now and names the cause — most usefully that a browser will only install a site served over HTTPS, which is also why the service worker would not have registered, so one cause was showing up as two mysteries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
92d525b3ab |
TipTap editor, version history, search, attachments — and a cleanup
The app is meant to run in daily private use now, so this pass was mostly
about the ways it could quietly lose data.
Reliability and security:
- the server no longer invents a JWT_SECRET or ENCRYPTION_KEY when one is
missing; it refuses to start. The old fallback silently made every stored
note unreadable after a restart.
- notes are encrypted with AES-256-GCM (v2:); existing CBC bodies still read.
- login is rate limited and compares against a dummy hash when the user does
not exist, so timing does not reveal who is registered.
- a server response issued before a local save is never adopted. This had
already eaten drawing strokes and note links; sync.hasPendingWrites() now
guards every path that takes a body from the server.
Editor and features:
- TipTap replaces the contenteditable editor: tables, task lists, and links
to other notes and notebooks, with a backlinks bar on the target.
- content format v2 ({v:2,doc}); v1 {html,tasks} bodies migrate on open.
- version history: the state before each write is snapshotted, coalesced
into 5-minute windows, 50 versions or 90 days per note.
- full-text search runs in the client, because the server cannot read
encrypted bodies and a server index would defeat the encryption.
- image attachments are stored by SHA-256 and deduplicated; the client
fetches them authenticated and caches the blob, so they work offline.
- drawings are vector strokes in a fixed 1200x1600 page space and now look
the same on every screen (0.0-0.2% deviation across four widths).
- login is by username; existing email accounts migrate automatically.
Operations:
- data lives on host bind mounts, not Docker volumes, so Proxmox Backup
covers it. Back up .env with it — without ENCRYPTION_KEY the dump is
unreadable.
Structure:
- NoteApp.jsx is gone. App.jsx now holds only where the user is; note data,
search, layout, history and drag each live behind a hook, and the four
panes are stateless.
- the whole data layer is TypeScript, with 81 unit tests concentrated on
the places that have actually broken.
- fixed along the way: the browser back button had a dead press, because
restoring a history entry pushed a duplicate one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|