Commit Graph
13 Commits
Author SHA1 Message Date
marcandClaude Opus 5 9e4677c70b Patterns on the covers, a search box that is simply there, roomier margin notes
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>
2026-08-12 00:29:00 +02:00
marcandClaude Opus 5 228552d453 Let a drawing run over as many pages as the note
The ink was stuck on page one while the writing carried on without it,
which stopped making sense the moment a note could run to four pages.
Every page is exported anyway, so every page can be drawn on.

Pages are numbered the way the PDF reader numbers them, and only pages
that carry ink are stored, so the keys are the drawn pages. While drawing
there is always one blank page past the end: pages otherwise come from
how far the writing runs, and to draw on page three you would first have
had to write three pages.

A drawing that lives entirely on page one is still written in the old
single-page shape. Nothing requires that — it means an ordinary note is
stored exactly as it always was, and a device running an older build
still shows it.

The bin clears the page being looked at. Undo and redo work on the
drawing as a whole, because one Undo button that means four different
things depending on where you scrolled is not a button anyone can use.

And a real bug found on the way out: the exporter has written
drawings/<note>.png since the interface was put into English, while the
importer was still looking for zeichnungen/<note>.png. A drawing exported
by this very app came back from its own archive as nothing at all. Both
names are read now, and so are the further pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:25:56 +02:00
marcandClaude Opus 5 fed53772aa Set the note left, count its pages, choose its typeface, align its text
The column sat in the middle, so it moved as the window was resized and the
start of every line landed somewhere new. It is against the margin now.

The toolbar counts pages in the words the PDF reader uses, with a hairline where
each ends. Pages are drawn, never stored: the note stays one document, Markdown
has no page, and a check asserts none reaches the stored body. The current page
is the one under the middle of the window — measuring from the top claimed "page
1" while you stood at the foot of a two-page note, because a window is shorter
than a page.

The typeface is a per-account setting rather than a per-browser one, because a
choice about how your own notes are set should follow you to the phone. The
server takes serif, sans or mono and drops anything else; that column is written
by a browser, so it is an input like any other.

Alignment is a small extension of our own — one attribute, a parse rule, a render
rule — so the schema every stored note is validated against stays in files this
project owns. Markdown has no alignment: what CommonMark allows is embedded HTML,
so non-left blocks travel as `<div align="…">`, the one form most readers still
honour, and the importer puts it back on the paragraphs. Left is never written.

The full run caught a crash I had put in: PdfWorkspace used `noteFont` without
taking it as a prop, which the build cannot see and the type checker does not
reach in .jsx. It killed the notes panel beside a PDF, and `offline` failed with
it because the whole app was broken.

224 unit tests, 263 browser checks across twelve suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 17:17:13 +02:00
marcandClaude Opus 5 52bd2b8f3d Put the interface into English, and give the notebook one way in
The interface had drifted into two languages — the frame in English, everything
added later in German — which is worse than either, since the same idea appeared
under two names depending on when that part was written. Sorting by title still
uses the German collation, so Übung 2 comes before Übung 10: that is about the
notes, not the chrome.

Two archive folders were renamed with it (bilder/ → images/, zeichnungen/ →
drawings/). The importer reads both spellings, and both the old and new marker
line for a drawing — archives exported before this are on disk, and an import
that quietly dropped their drawings would be the worst kind of tidy-up.

Making a note, uploading a PDF and importing Markdown were three buttons side by
side, two of them icons you had to hover to identify. They are the same act —
putting something new in this notebook — so they are now one + menu.

The search box moves out of the sidebar and into the note list, labelled with the
notebook it narrows. Nothing changed about what it does: it has always searched
only the notebook you are in, but from the sidebar it read as searching
everything.

A separator no longer carries a sentence explaining the order the sort menu
already names. Renaming joins removing on the right-click menu — a left-click on
the label started an edit, so the row could not be touched without falling into
one.

218 unit tests, 238 browser checks across eleven suites. Two of those caught real
breakage from this change rather than needing a translation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 02:17:22 +02:00
marcandClaude Opus 5 0a7cf26b28 Group the two visibility switches, and put the drawing tools back where they belong
"Zeichnung" and "Randnotizen" are two halves of one question — how much of your
own work do you want to see right now — and they sat at opposite ends of the
toolbar, each beside the tool that made them. They are now one pair in one shape
at the right-hand end. Each still appears only once there is something of its
kind to hide, so the pair can be lopsided, which is honest.

The drawing tools had drifted past the pin controls as those were added, so
switching to Zeichnen made them appear somewhere other than where the eye
already was. They follow the button that turns them on again.

A separator no longer carries a ×. It sat a few pixels from the label you click
to rename, putting the gesture that tidies the list next to the one that destroys
part of it — so removal is a right-click behind a confirmation that says the
notes are not affected, which is how everything else in that list is acted on.

218 unit tests, 237 browser checks across eleven suites. The toolbar checks
compare measured positions rather than presence, since the complaint was about
where things were, not whether they existed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 01:24:25 +02:00
marcandClaude Opus 5 4c1b632029 Link the margin notes from the notes, and let them be hidden
The linking went the wrong way round. A remark in the margin is short by design;
when there is more to say about that passage it belongs in the notes beside the
document — and it has to be possible to say *which* passage. So the pins are
offered to the editor's `@` menu exactly like notes and notebooks are, labelled
with their page, and following one scrolls the document to the spot and opens the
remark. Pins are unhidden first: a link leading to something invisible would read
as broken.

A pin link is the same node with kind "pin", and is deliberately kept out of the
links reported to the server. Its target is a marker inside this note's own
overlay, so no note has that id, and reporting it would fill the backlink table
with rows matching nothing.

They can also be hidden altogether. Reading a document as it was printed is a
normal thing to want; the toolbar takes the markers and the column away together
and the page takes the width back.

The other direction — making a note out of a remark — is removed, along with its
picker and the pin's noteId: 357 lines fewer than before. The unmount flush it
turned up stays, since a drawing save was genuinely being dropped by navigating
away inside its debounce.

218 unit tests, 231 browser checks across eleven suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:00:46 +02:00
marcandClaude Opus 5 502aecfc92 Let a margin remark become a note, and lead to it ever after
Some remarks turn into a page of their own. "Notiz anlegen" on a margin card
makes one, titles it from what was written, and goes there; the pin keeps its id
so the passage leads to that note afterwards. The note is created with a link
back to the document, so the connection reads both ways and the document lists it
among its backlinks — a note born from a passage should not be a note with
nothing on it saying which passage. An existing note can be linked instead, and a
link can be let go of without touching the note. Both are offered on the phone
sheet too.

Two older bugs stood in the way and are fixed:

`openLinkTarget` finds a note's notebook by looking it up in the notes list, and
a note created in the same tick is not in the copy that callback closed over — so
jumping to a fresh note went nowhere at all. Creating one now navigates by the
notebook it already knows.

A drawing save waits 700ms for the next stroke, and leaving the document inside
that window dropped it. That is precisely what turning a remark into a note does,
so the link would have been lost on the way out. The viewer flushes a pending
write when it unmounts, which also covers drawing and navigating away quickly.

215 unit tests, 229 browser checks across eleven suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:40:38 +02:00
marcandClaude Opus 5 2cee00e38e Fix a permanent black line in the note list, and add separators
The drop indicator was a `borderTop` set in the same style object as a `border`
shorthand. Clearing the longhand resets its width and colour to the defaults
while the shorthand's `solid` stays, so a row kept a 2px black line for good —
measured at rest, before any drag. It is now its own element, which also stops
the list twitching as a row grows and shrinks by a pixel.

That element then turned out to be a dead strip of its own: it pushes the row
down as it appears, so the pointer ended up over the line rather than over the
row and releasing there dropped on nothing. The drag handlers moved to the whole
row and the indicator no longer takes pointer events. A strip below the last row
means "put it at the end", which otherwise needs a drop onto the final note.

Separators are lines drawn between notes, optionally named, stored on the
notebook beside its sort mode. They drag like notes do, and their position is
"how many notes come before me", so they hold their place as the notes around
them are renamed or edited. Shown only in the manual order — in "newest first"
the notes would slide past the line on every edit.

"All Notes" and "Starred" no longer offer a hand-made order: they are questions
asked of every notebook, so there is nothing for an arrangement to belong to.

199 unit tests, 190 browser checks across eleven suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 01:45:38 +02:00
marcandClaude Opus 5 0a0864c58b Sort a notebook, import Markdown back, and stop the page shrinking in Draw
The paper measurably shrank on switching to Draw — 733px to 460px on the same
screen. The editor was a flex item at its default `flex: 0 1 auto`, so its width
was its widest row's: the toolbar. Swapping a dozen formatting buttons for six
drawing ones collapsed it. It now fills the pane, so the width is a property of
the window.

Each notebook remembers how its notes are ordered, on the server rather than per
device. The manual order has to survive being looked away from, so `reindex`
renumbers every note in the notebook rather than the filtered view, and dragging
is switched off while a search narrows the list. Reordering has its own route:
`PUT /notes/:id` would bump `updated_at` and snapshot a version, so a drag would
move a note to the top of "last changed" and spend a slot of its history.

Markdown and our own ZIP archives come back as notes, with their images
re-uploaded as attachments. A drawing returns as the PNG it was exported as —
vectors are not recoverable from pixels — and the import says so rather than
leaving it to be discovered. Writing the parser turned up a real bug in the
exporter: blocks were emitted with no blank line between them, so a list after a
list became one list and a quote after an item was read as part of it.

Creating or importing from "All Notes" now asks which notebook instead of
silently taking the first one. The web-link prompt is a real dialog: a bare
domain becomes https, javascript: is refused with a reason. The PDF viewer's
hide-drawing control is a labelled button rather than a filled-or-hollow dot,
and the export icon points down into a tray like every other download.

186 unit tests, 179 browser checks across eleven suites, including a new
`offline` suite that loads the app cold with the network off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:38:03 +02:00
marcandClaude Opus 5 2986bcb620 Read imported PDFs in the app
Stage three: the viewer. Drawing on pages comes next.

pdf.js stays out of the bundle everyone downloads — it is larger than the
rest of the application put together, so it is behind a dynamic import and
a session that never opens a PDF never fetches it. Main bundle grew by
8 kB for the viewer shell; the 162 kB library arrives only when needed.
The worker is bundled rather than pulled from a CDN, because a viewer
that stops working during someone else's outage is not one you can rely
on offline.

Two things that only showed up once it ran in a browser:

- pdf.js 6 calls Uint8Array.prototype.toHex, which only very recent
  browsers have. Anything older failed with "n.toHex is not a function",
  which says nothing about the cause. Now on the legacy build, which
  compiles those away — a little larger, and the difference between
  working everywhere and working on this year's browsers.
- nginx has no MIME type for .mjs and served the worker as
  octet-stream, which a browser refuses to run as a module.

Pages are laid out at their true proportions before anything is drawn, so
the scrollbar is honest immediately and a document mixing portrait and
landscape keeps both — the first version scaled every page to the widest
one, which the suite caught by checking the two aspect ratios differ.

Bytes come from the authenticated attachment route and are cached in
IndexedDB. Content-addressed storage means a cached copy can never be
stale, so a document opened once needs no connection again. Verified with
the network switched off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:36:04 +02:00
marcandClaude Opus 5 2611d87f8c Export notes as Markdown or PDF
The reason to build this is not a missing feature — it is that trusting a
program you maintain alone with years of notes should not mean the notes
can only ever be read by that program.

Reachable from a note's toolbar, a notebook's context menu, and Account →
Alle Notizen exportieren.

Markdown carries over everything with an equivalent: headings, lists,
quotes, code, tables, and task state as `- [x]`. A link to another note in
the same export becomes a relative link to its file; a link to a note
outside it degrades to the plain title rather than leaving a dead
reference. Notes with images or a drawing arrive as a ZIP so the files
travel along, and a multi-note export gets a README index.

PDF goes through the browser's print dialog rather than a bundled PDF
library — the browser already has a good one, and it is the only format
where drawings sit inside the document. Markdown gets them as a PNG
beside the text, with a line saying so, because dropping them silently
would be the wrong kind of quiet.

Two decisions worth recording:

- the ZIP writer is eighty lines here rather than a dependency. Note text
  is small and images are already compressed, so compression bought
  nothing, and stored entries are a handful of fixed-layout records.
- Markdown escaping only covers what changes meaning mid-line, plus a
  leading list or heading marker. Escaping every character Markdown ever
  uses turns a sentence into `Hallo\. Alles gut\!`, which defeats the
  point of choosing a readable format.

42 unit tests on the serialiser and the archive format, plus a browser
suite that unpacks a real download and reads the bytes back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 14:46:53 +02:00
marcandClaude Opus 5 68e184163e Remove public sign-up; accounts are created by an administrator
This is a private instance, so anyone able to reach the login page could
previously give themselves an account. The registration route is gone
rather than hidden, and the login screen no longer hints that accounts
can be made.

Accounts now come from an admin under Account → Administration: create,
delete, and reset a password. Appointing further admins is deliberately
not offered — with one admin it is not needed, and it is the one power
that turns a compromised account into a lost instance.

The first admin comes from INKNOTE_ADMIN_USER / INKNOTE_ADMIN_PASSWORD,
used only when the database has no users, or to choose whom to promote
when an older database has users but no admin. Once an admin exists both
are ignored, so leaving them in .env cannot reset anything.

Two things that would otherwise have been quietly wrong:

- a deleted user's token stayed valid until it expired, because tokens
  are signed rather than stored and there was no session to revoke.
  authMiddleware now checks the account still exists, so an open tab
  stops working the moment the account is deleted.
- deleting an account takes its notes, notebooks, drawings and version
  history with it via ON DELETE CASCADE. Verified: no orphaned rows in
  notes, notebooks, sync_log, attachments or note_versions.

Deletion asks for the username to be typed back, since it destroys data
that version history cannot bring back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 12:53:08 +02:00
marcandClaude Opus 5 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>
2026-08-07 17:00:14 +02:00