Commit Graph
19 Commits
Author SHA1 Message Date
marcandClaude Opus 5 8c36c6121b Ask what a single note's export should contain, and fix drawings never being in one
Two questions instead of one, because the answers depend on each other:
Markdown holds text only, a PNG holds the drawing only, PDF holds both.
Listing every format flat would have offered combinations that cannot
exist. A note without a drawing skips the first step rather than showing
it mostly greyed out.

While testing that, the drawings turned out never to have made it into
any export at all — not the ZIP, not the PDF. strokeDocOf() *builds* a
stroke document from a bare array, and it was being handed one that was
already a document, so the strokes ended up nested one level too deep and
every length check read undefined. Nothing complained; the export simply
came out without the drawing. It went unnoticed because the notes the
earlier suite exported had none.

Now uses docStrokes(), and the suite draws on a note before exporting it,
checks the PNG's magic bytes, and checks it is 159 KB rather than an empty
white page — a check on the file merely existing would have passed against
the broken version too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 17:37:22 +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 311e992496 Move the browser suites into the repository
They lived in a scratch directory under /tmp, which was cleared overnight;
four of the five were simply gone. Whatever they were worth, they were
worth nothing kept there.

Rewritten against a shared lib.mjs. The old ones registered their users
through the sign-up form, which no longer exists — each suite now creates
a throwaway account through the admin API and deletes it afterwards, so a
run leaves the database as it found it. The previous generation left nine
accounts behind; those are gone too.

Three test hooks went into the app, each of which earns its place beyond
testing: aria-label on the note title (the sidebar search box is also a
text input, and a screen reader could not tell them apart either),
data-note-row on list rows, data-user on admin rows.

Two suites were flaky in ways worth naming, because both were the test
misreading the app rather than the app misbehaving:
- ticking a task then inserting a note link put the link inside the task
  list, since Control+End lands there. Verified against the server that
  the tick itself persists correctly; the suite now leaves the list first
  and asserts where the link ended up.
- the note-title lookup matched the sidebar's Trash entry, which wears the
  same emoji as a note row.

56 checks across five suites, all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 14:30:08 +02:00
marcandClaude Opus 5 41ba99c017 Node 20 → 24, and say what is wrong when the data directory is not writable
Node 20 left maintenance in April 2026, so a production instance was
running an interpreter that no longer gets security fixes.

The upgrade needed better-sqlite3 as well. On Node 24 the version we
pinned (^11) aborts inside the native addon —

  node[1]: void node::RemoveEnvironmentCleanupHook(...) at hooks.cc:142
  Assertion failed: (env) != nullptr

— so bumping only the base image would have shipped a server that dies on
shutdown. Raised to ^12, which handles Node 24; verified against a copy of
a real database that existing encrypted notes and version history still
decrypt, and that SIGTERM now closes cleanly.

Separately: a bind-mounted data directory the container cannot write to
surfaced as "SqliteError: unable to open database file" out of
better-sqlite3, which points at the database when the problem is on the
host. Startup now checks both directories and says which uid it runs as,
who owns the directory, and what to run to fix it.

Frontend build now copies the lockfile and uses npm ci, so the image is
built from the dependency versions that were tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:40:45 +02:00
marcandClaude Opus 5 982de53c77 Explain the ${VAR:-fallback} syntax where it is actually read
The line reads as if /srv were appended to whatever you set. Says so
right above it now, and drops the comment block that repeated the mkdir
instruction already in the header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:25:09 +02:00
marcandClaude Opus 5 6dcb77fa73 Shorten .env.example; move the long explanations to the readme
76 lines of commentary for five settings was more wall than help. The
file now says what to set; the reasoning behind the data directory, and
the two ways of putting it under /root, live in the readme where there is
room for them.

Also brought the deployment section up to date: it still told people to
export secrets by hand and run docker-compose, with no word about the
first admin, which is now the step that decides whether you can log in at
all.

INKNOTE_DATA_DIR stays commented out — one .env serves both compose
files, so setting it would drag local development off ./data too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:17:30 +02:00
marcandClaude Opus 5 4e93259c04 Document putting the data directory under /root
/root is mode 0700 and the container runs as uid 1000, so it cannot even
traverse it — the server fails to write no matter who owns the directory
underneath. Both ways out are written down, with the trade each makes:
chmod o+x /root keeps the unprivileged container and still refuses ls,
running as uid 0 avoids the chmod but puts a break-out on root.

Also noted that the path has to start with / or ./, since anything else
is read as a named volume name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:13:45 +02:00
marcandClaude Opus 5 a61e6901b1 Move .env.example where Compose actually looks, and make it true
It sat in backend/ while Compose reads .env from the repository root, so
the "cp .env.example .env" in the compose header pointed at nothing.

It also documented four settings that Compose never forwarded to the
container: ALLOWED_ORIGINS and the three version-retention knobs would
have been set in .env and silently done nothing. They are passed through
now, unset meaning the server's own defaults. Checked both directions:
every variable Compose reads is documented, and nothing documented is
ignored.

Dropped PORT from the example — the bundled nginx proxies to 4000 by
name, so changing it could only break that. Added the data-directory and
uid/gid settings, which were read but undocumented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:09:29 +02:00
marcandClaude Opus 5 d01aa85ff9 Clean up the production compose file
Three leftovers: a named-volume block that nothing has used since the
move to bind mounts, the obsolete version key, and a comment claiming the
admin variables only apply to an empty database — they also decide whom
to promote when upgrading one that has users but no admin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 13:05:21 +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 55a6b3caab Correct the registry namespace to the Docker org
The previous commit moved the images to honey/, which was wrong: the
images have lived under the Docker organisation since February, alongside
learning-tracker and whopays, and are linked to this repository there.
Only the placeholder 'marc' was never real.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:05:48 +02:00
marcandClaude Opus 5 dc33d614ad Point the registry at the namespace that actually exists
The images were tagged gitea.../marc/..., a placeholder that was never
changed and is not a Gitea account, so every push failed with a 404 and
the images had in fact never been published. The registry owner is honey.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:02:19 +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
HoneyandClaude Sonnet 4.6 d97591b242 Improved drawing sync: MIME detection, WebP/JPEG support, sync log + frontend updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 13:15:25 +02:00
Honey a8779d7be1 Alpha Version of Syncing and Desktop/Anroid PWA 2026-02-18 20:07:10 +01:00
Honey 8cd8133f9e Working Web Server changes and Docker Image 2026-02-18 01:28:10 +01:00
Honey 443184a6fa added docker files and compose 2026-02-17 21:09:58 +01:00
Honey 4709096ab3 First Commit 2026-02-17 19:25:52 +01:00
Honey fa15d48a52 first commit 2026-02-17 19:25:18 +01:00