Three things Marc asked for, and one of them turned out to be the hinge
the other two hang from.
**A chapter is a row like any other.** Same grip, same drag, same place
in a hand-made arrangement as a note or a separator. That needed notes
and chapters numbered from *one* counter: numbered separately they are
two sequences that cannot be interleaved again, and a chapter dragged
between two notes springs back to the top. Positions therefore have to be
named rather than read off an array index, so POST /notes/reorder now
takes `positions: [{id, order}]` beside the older `ids`, and chapters
have a reorder route of their own.
**Two ways for a chapter to open**, chosen per account beside the
typeface, for the same reason that one lives there: it is a choice about
how your own notes are laid out and it should follow you to the phone.
Beside the list, its notes in a further column the way OneNote lays out
pages — the notebook's own list carrying on to the left, chapter rows and
all. Or unfolded under the chapter's own row, indented against a rule in
its colour. A phone has room for neither and fills the list instead, with
a way back, which is what it did before there was a choice.
**Its own colour**, from eight named ones rather than the full palette:
the point of chapters is that they are different places, and twenty
near-identical blues do not help anyone tell them apart.
The colour is validated on the way in — it is written by a browser, so
only a plain #rrggbb gets through and anything else becomes no colour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things were drawn on every cover regardless of what was chosen, and
between them they are most of why a shelf of notebooks looked alike.
A 45-degree stripe texture was baked into the cover at 1.5% white. It was
meant to be imperceptible; on the darker gradients it bands visibly, and
since covers gained patterns of their own it was a second pattern
fighting the first. Removed.
And the same pair of rings sat in the middle of every one. The mark
belongs to the design now — a ring, a square, a diamond, a dot, a cross,
an arc, three bars — and the designs whose pattern already fills the face
wear nothing at all. Drawn with borders rather than an image, so each
takes its cover's own accent and stays sharp at any size.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A cover is a colour *and* a pattern now, chosen in turn. Offering every
combination as its own entry meant fifteen colours times a dozen patterns
in a grid nobody could take in — and in practice only the handful someone
had thought to write out. Each pattern is previewed over the colour in
hand, so what is on screen is the cover as it will be. `cover_pattern` is
its own column: they are two decisions, and folding them into one number
would mean renumbering every cover the day a pattern is added.
For one day the patterns *were* twelve appended entries. Those indices
are read back as the colour-and-pattern pair they were drawn as, so
nothing chosen then looks different now.
And three separate reasons the install button never appeared, none of
them in anyone's configuration:
- `beforeinstallprompt` fires once, early — usually before React has
mounted — and is never fired again. A listener added from inside a
component simply missed it. It is caught at the top of index.html now.
- A `<link rel="manifest">` is fetched with credentials omitted unless it
says otherwise. Behind a reverse proxy that authenticates in front of
the app, that one request comes back as the proxy's login page and the
browser reads it as an invalid manifest — while the app itself works
perfectly, because its own requests carry the cookie.
- The service worker cached whatever came back, so a lapsed session could
be stored as the application itself. The app would then "work offline"
by showing a login screen it could never get past, curable only by
clearing the site's data. Only successful, unredirected responses are
cached now, and the cache name is bumped to drop anything already
stored under the old rules.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four small things about the shape of the app.
Covers can be patterns as well as colours — ruled paper, graph paper,
pinstripe, bands, dots, cross-hatch, linen, washes, a ledger — drawn
entirely with CSS gradient layers. No image files, nothing to upload,
crisp at any size, and offline like everything else. A cover is stored as
an index into that list, so the new ones are appended and never inserted:
renumbering would silently repaint every notebook anyone already has.
Two things the patterns taught. One idea per cover: two crossed diagonals
meant as a chevron came out as noisy plaid at the size a cover is
actually read, and were replaced with plain bands. And the grid has to
scroll — with 27 covers the dialog ran past the bottom of a laptop screen
and took the Create button with it, which a check now holds at 1440x800.
The search box in a notebook is simply there now, and the button that
used to open it is gone. A magnifying glass cost a click and having to
remember that searching was possible at all; a search box is one line and
says for itself what it does.
Margin notes went from 208x74 to 288x112 — from three short lines to
about five. Three lines is a caption, not a remark on a passage, and the
space beside the page was sitting mostly empty.
And the notebook edit dialog was still part German, and would not say
what the server's default number of versions actually is. It names it
now, read from the server rather than written into the interface: it is
set with MAX_VERSIONS_PER_NOTE, and a hardcoded "50" would quietly start
lying the day that changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sheet was not what it wanted to be, so writing is plain text again: about 73
characters to the line, centred, no border and no shadow. The fixed page belongs
to the canvas — a stroke has to land in the same place on every screen, which
needs one agreed page — so it appears exactly when something is on it or about
to be: in Draw, or when the note already has a drawing. The cap lives in the
stylesheet rather than on a container, so it holds on a page as well as off one.
Emoji are gone from the note rows and the sidebar. They are drawn by the
operating system, so they looked different on every machine and were loud enough
that a row's picture caught the eye before its title did. Rows are tighter by
three pixels of padding.
The trash button no longer stands permanently in the danger colour beside the
star, which made the most destructive control the loudest thing on screen. It
sits behind a divider and turns red under the pointer, when it is about to be
used.
On a phone the toolbar's fourteen controls scrolled sideways, so the ones at the
end could only be reached by dragging the bar and nothing said they were there.
Nine remain, plus a menu holding the headings, quote, code and undo.
A new `sync` suite proves what was assumed: a PDF written on one device — the
document, the drawing and the pinned notes — reaches a second one. Two browser
contexts are two devices.
Both search suites read a row's title as the second line of its text, which only
worked while the icon was itself a text emoji; they read it by name now.
218 unit tests, 249 browser checks across twelve suites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>