Files
marcandClaude Opus 5 4013e959d6 The same page on every screen
The text is laid out on a page of one fixed size and the sheet scaled to
the screen, so a phone breaks it exactly where a desktop does — twenty-one
lines to the page on both. The type size is computed from the page and the
line count rather than chosen.

A PDF's full screen is the browser window, not the screen, and its button
sits beside the zoom. "Recent Notes" is stamped by every write, not only by
a rename. And a PDF that will not open says which of the four reasons it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 13:23:03 +02:00

509 lines
27 KiB
Markdown

# Changelog
What changed, newest first — and the digests of every image that went out, so it
stays possible to tell what is actually running on the server. Digests are read
back from the registry (`docker buildx imagetools inspect`), never taken from the
output of `docker push`.
Kept short on purpose. A day's work usually went out as several images, and a
round that repaired what an earlier round the same day had broken says nothing
worth keeping on its own — so each day is one entry, describing where it ended
up, with every digest of that day listed. Where the server image is named as
unchanged in substance, the digest still moved: `build.sh` builds both images and
a rebuild is not bit-identical.
---
## 2026-08-26 — The same page on every screen
**Images** — web `sha256:e475c080…`, server `sha256:49a98c66…`
The server image carries no change of its own; `build.sh` builds both.
**Changed**
- **Twenty-one lines to the page, and the same twenty-one everywhere.** The text
is now laid out on a page of one fixed size and the whole sheet is scaled to
the screen, instead of being set in screen pixels. Set that way, a phone and a
desktop disagreed about everything that matters — the same note broke into
different lines on different pages depending on what you read it on, and a
phone page held eighteen lines where a desktop held far more. The type size is
computed from the page and the line count rather than chosen, so the count
survives a change to either; a desktop therefore shows larger writing on a
smaller-looking sheet, the way a ruled notebook app does.
- **Full screen for a PDF means the browser window, not the screen.** Asking the
browser for its own full screen took the tabs and the address bar with it,
which was more than was wanted. The overlay over the app — the part that
removes the sidebar, the list and the title bar — is the whole feature now.
The button moved next to the zoom, where the other question about how much
room the document gets already lives.
**Fixed**
- **"Recent Notes" is now sorted by when you last wrote in a note**, not by when
you last renamed one. The store always stamped every write; the lists on screen
read the app's own copy of the notes, and writing *in* a note left that copy
untouched — so the dashboard and the "Last changed" sort showed the order from
the last reload.
- **A PDF that will not open says why.** One message covered every failure and
guessed at the cause ("offline?"), which was usually wrong and left a report of
"it will not open on my phone" with nothing in it. The status now travels with
the failure: a missing file, a server that refused, a server that could not be
reached and a file that will not parse are four different messages.
## 2026-08-25 — Colour, a page you can see the edge of, and a document on its own
**Images** — web `sha256:3e1b47e1…`, server `sha256:89e474cc…`
(earlier that day: web `783ccf92…`, `d7c66235…`; server `2141f145…`, `308ed1b7…`)
The server changed earlier in the day for the bar's runtime configuration; this
last pair carries everything below and no backend change of its own. The two
entries under it are the same day's earlier rounds, kept apart because they are
about different things.
**Added**
- **A marker pen and a colour for the letters**, both under one button in the
toolbar — two buttons would be two panels for one question. A highlight is a
`<mark>`, the element that *means* highlighted, and marked text is painted with
dark ink whatever the theme, the way a real marker leaves the writing alone.
Markdown has no colour, so both travel as HTML like underline does, and both
are read back — including a bare `<mark>`, which is what most other writers
emit.
- **The PDF fills the screen.** The button asks the browser for its own full
screen *and* lays the document out over the app; either alone is half the job,
and a browser that refuses the first still gets the second. Escape leaves, and
the button stays visible while full — a way out that is only known is not a way
out.
- **Dragging in from the left edge shows the sidebar** on a phone or tablet, and
dragging it back hides it. It must start at the edge, travel far enough to be
meant, and be more sideways than downwards; without the first rule every
sideways drag inside a note opens it, without the third every scroll does.
**Changed**
- **The dashboard leads with what you were last writing**, the shelf of notebooks
second — and "Recent" is sorted by time whatever order the notebooks are in.
It is a question about time, and a hand-made arrangement is the wrong answer.
- **Tapping the blank part of a page starts writing there.** The text is only as
tall as what has been written, so most of a fresh page is beside the editable
area rather than in it, and a tap down there landed on nothing at all: no
caret, and on a phone no keyboard. The caret goes to the position nearest the
tap rather than to the end.
- **The page has an edge on a phone.** The paper runs to the edges of the screen
there, but it was drawn with no border and no shadow, so nothing said where one
page ended and the next began.
---
## 2026-08-25 — The bar that ties the apps together, and Ctrl over a PDF
**Images** — web `sha256:783ccf92…`, server `sha256:2141f145…`
(earlier that day: web `d7c66235…`, server `308ed1b7…`)
The server image changed for the bar's runtime configuration; the second pair of
digests carries the PDF work as well, and no backend change of its own.
### The bar
**Added**
- **A strip along the top that leads to the other apps.** It is served by
HoneyOS, so what it looks like and which apps it lists are settled there —
InkNote only says where to find it. `HONEYBAR_ORIGIN` in the `.env`
switches it on, empty leaves everything exactly as before.
- **`GET /api/public/config`** hands out that origin, and the key that goes with
it when HoneyOS lives on a different site. Runtime, not build time: an image
is pulled ready-made, so a value baked in at build time could never be changed
from the `.env` — and a service worker keeps whole responses, headers
included, so anything frozen into `index.html` outlives the setting in the
browser. Aether learned that one the hard way first.
**Changed**
- **The app now measures its height against the bar**, in `#root` and in the
four places that asked for the full window. With `overflow: hidden` on the
body the surplus would not have scrolled — it would have been cut off at the
bottom, silently. And `#root` padded itself around the phone's notch, which
the bar now covers, so that gap would have been counted twice. Without a bar
`--honeybar-h` is `0px` and every one of these is what it was.
### Reading a PDF
**Added**
- **Ctrl (or ⌘) and the wheel zoom the document**, as in every other viewer, and
a trackpad pinch with it — it arrives as the same event. The listener is
registered by hand rather than through `onWheel`: React attaches wheel
listeners passively, a passive listener may not call `preventDefault`, and
without that the *browser* zooms the whole application instead, toolbar and
all. **The point under the pointer stays under the pointer**: you are pointing
at a word and asking for that to grow, not to be sent to the top of the page.
**Fixed**
- **Zooming with the buttons drifted.** A zoom changes how tall the document is,
so the scroll position means something else afterwards, and nothing was holding
anything still — three presses of + and you were elsewhere in the file. They
hold the page being read now, which is what "show me this page larger" asks
for.
---
## 2026-08-22 — A bigger face, and the theme where the settings are
**Images** — web `sha256:fe35b0e7…`, server `sha256:a557fb4d…`
The server image changed: `theme` is now one of the settings an account keeps.
**Changed**
- **The profile picture fills the account button**, 34px instead of 20 — a face
at twenty pixels is a smudge — and the same size in the account panel.
- **Light or dark moved into Account → Profile**, beside the typeface and how a
chapter opens, which is the kind of thing it is. It used to be a sun in the
corner of the sidebar: reached for by accident, and gone again on the next
device. That corner now holds one button, the way into the account.
- **And it stays.** The theme was pure screen state before, so every reload
landed on dark whatever had been chosen. It belongs to the account now and
follows you to the phone, with a copy in the browser so a reload does not flash
the other one while the account is being fetched. The server takes `dark` or
`light` and drops anything else, like every other setting written by a browser.
---
## 2026-08-21 — Asking for the picture properly
**Images** — web `sha256:4310304d…`, server `sha256:7a486929…`
The server image changed; the web image only because `build.sh` builds both.
**Fixed**
- **The profile picture never arrived**, and two assumptions of mine were why.
The picture was only looked for in the ID token — a provider may keep its token
lean and hold the rest at its `userinfo` endpoint, which is what PocketID does,
and that endpoint was never asked. And the image was then fetched with no
credentials at all, while providers commonly keep avatars behind their own
login. The endpoint is now asked when something is missing (and its answer
trusted only if the `sub` matches the verified token), and the access token
goes with the picture request — but only when the picture lives at the
provider, because a token sent to a third party is a credential given away.
- **And it failed in silence**, which is the part that actually cost the time:
there was no way to see which of those two it was. Every outcome is now one
line in the server log — stored, no claim, refused, not an image, too large,
unreachable.
The test provider now behaves like a lean one: it leaves the picture out of the
token and hands it over only against the access token, so the check passing is
evidence for both new paths.
---
## 2026-08-21 — The face from the provider, and whose business a password is
**Images** — web `sha256:e525b11d…`, server `sha256:95d56c46…`
The server image changed: a table for profile pictures and the route that serves
them.
- **The profile picture comes along.** When the provider sends one — PocketID
does — the server fetches it at each sign-in and keeps it, encrypted, in a
table of its own rather than in `users`, where a blob would ride along with
every `SELECT *` for the sake of one corner of the screen. It is served back
through the API: nothing on the page talks to the provider, no third-party
cookie has to survive for a face to appear, and the picture keeps working when
the provider does not. It shows in the sidebar, in the account panel and beside
the name in the administration list, and arrives with the **next** sign-in —
the one moment this app speaks to the provider at all.
- **An account that signs in through the provider has no password tab**, and that
now includes one that had a password before it was linked — the usual case for
whoever set this up, and the reason the tab was still there. The password it
kept is the emergency door rather than a second thing for its owner to
maintain: it is set by an administrator, and the account panel says so.
---
## 2026-08-18 — Which accounts belong to the provider
**Images** — web `sha256:5fb51ed4…`, server `sha256:ce1f4649…`
- **The administration list marks every account the provider can sign in**, not
only the ones it created. Those are two different things and the badge had been
hung on the wrong one: an account that already existed and was *linked* — which
is what happens to the account you were using when you first sign in this way —
showed nothing at all. The row says which it is: an account with no password
here says so, and the button offers to **give** it one rather than to reset
something that does not exist, with a line explaining that this adds a second
way in and takes nothing away.
- **The account panel says how you get in.** The password tab is gone for an
account that has none — it was already, and the server refuses that route too —
but nothing said why. It now names the provider, and tells an account that has
both that the password still works.
---
## 2026-08-18 — A second way in
**Images** — web `sha256:232982cb…`, server `sha256:5ed577b1…`
The server image changed: OpenID Connect, and an `oidc_sub` column on users.
- **Signing in through an identity provider**, when one is configured — issuer,
client id and secret, and the address this app is reached at; without them
nothing about it appears anywhere. It is a second way to the *same* token the
password login mints, which is why it fits in one file and needs no new
dependency: Node imports a JWK itself, so checking an ID token is a signature
and five claims.
- **The browser is handed a one-time ticket, never the token.** The code is
exchanged on the server, so the client secret stays there, and a token in a
redirect URL would otherwise sit in the history, in the proxy's log and in the
referrer of the next request. The ticket is good for a minute and for one use.
- **An account is made for somebody the app has not seen**, and an account that
already exists under that name is linked to the identity rather than
duplicated — by subject from then on, so either side can be renamed. Never an
administrator: those rights are granted in the app, and a group at the provider
must not be a way around it.
- **Both doors stay open.** An account made this way has no password here at all
— the interface stops offering to change one and the login route refuses it as
it refuses a wrong one, at the same speed — but an administrator can still set
one, which is the way back in the day the provider is unreachable.
- **A suite that signs in at a provider of its own** (`e2e/oidc.mjs`), which
signs real RS256 tokens with a key InkNote fetches and checks. It holds down
the four ways the journey must fail as well as the way it must work: a refusal,
a replayed ticket, a token signed with an unpublished key, and one addressed to
another client.
---
## 2026-08-17 — Quotes that hold more than a sentence, and one list of marks
**Images** — web `sha256:30f67832…`, server `sha256:6ffe4b58…`
No backend source changed this round.
- **A quote may hold whole blocks — and now has room for them.** A heading, a
list and a closing line inside a quote are ordinary blocks, but the rule that
gives the writing its rhythm reaches only the editor's own children, so inside
a quote everything stood jammed against its neighbour, with the items of the
list further apart than the blocks around them.
- **The same divide when it leaves as Markdown.** Blocks inside a quote were
written on consecutive lines, which every other reader folds into one: the
sentence after a quoted list became more of the last bullet. They are separated
by a **quoted** blank line now — `>` on its own, since an empty line would end
the quote — which is the same rule the top level has had since 11 August, and
reading one back gives the blocks it was written with, quotes inside quotes
included.
- **Marks are one list again.** There were two copies, in the note dialog and the
chapter dialog, and only one of them had ever been extended — so which marks
existed depended on which dialog you happened to open. One grid, and with it
twice as many marks, **divided under headings** now that there are too many for
one wall of pictures.
- **A mark nobody thought of can simply be typed.** The search box is a text
field, which on a phone is exactly where the emoji keyboard lives, so anything
pasted or typed into it is offered as a tile — one glyph with its modifiers, the
same eight code points the server keeps.
- **The changelog is a third of its length.** A day usually went out as several
images, and a round that repaired what an earlier round the same day had broken
reads as noise afterwards. One entry per day now, saying where it ended up, with
every digest of that day listed.
---
## 2026-08-17 — A notebook arranged by hand
**Images** — web `sha256:c5e460cf…`, server `sha256:ac3b0527…`
(earlier that day: web `b7c9f8b7…`, `c1bcdcc4…`, `b8bc284a…`, `18a68a2d…`;
server `a0fd140a…`, `6c9af404…`, `a66cc8eb…`, `febc4b93…`)
The server gained one column: `sort_mode` on chapters.
**Arrangement**
- **A drop lands the row *under* the one it was released on, and only there.**
Dropping above a row and below its predecessor are the same move described
twice, so every gap could be aimed at from two sides. A strip **above the first
row** covers the one place a drop *under* something cannot reach.
- **A new note appears at the very top and nothing else moves.** A row arriving
without a position left every separator counting rows that were no longer above
it, and a little more of the order came undone with each note. Every way in and
every way out — new, restored, moved, binned, or a chapter removed — now
renumbers the list.
- **A chapter is a place *and* a neighbour**: its header takes a row dropped on
it, its bottom edge is the gap under it. It is always in the user's own order,
and its notes are dragged under the same switch as the list beside them.
- **A chapter can be given an order of its own** — in the same dialog as its name,
mark and colour. In any order but the hand-made one it loses its lines and
handles, as the notebook does.
- **Two lines could not be put in one gap.** They carry the same position, and the
comparator claimed both directions at once for that case; the write that fixed
it was then discarded by the "did anything change?" test, which sorted before
comparing and so could not see a sequence.
**Elsewhere**
- ***Move to…* is divided under headings** — the notebook itself, *Chapters here*,
*Other notebooks* — each row wearing its chapter's mark or its notebook's cover.
The headings are deliberately not shaped like entries.
- **A separator and a chapter each have one *Edit*** instead of an entry per
property, and the separator dialog draws the line as it will look.
- **A link wears the mark of the note it points at**, in the text and in the `@`
menu, instead of one sheet-of-paper glyph for everything.
- **The mark, the title and the task-list box sit on the line they belong to.**
Boxes had been centred, which is not the same as letters being aligned; the
mark and title share a baseline, the checkbox is centred on the capitals and
sized in em, and the mark stands in the same column as the tools below it.
- **Grey letters ran down the bar while dragging a column edge.** An absolutely
positioned child in a cell of a `border-collapse` table sends Chromium down
another painting path, and the cell's own text came out a second time, squeezed
under the bar. The marker is never drawn now; the line is an inset shadow on the
cells the column is made of.
- **Opening a note flashed a long narrow page**, because the drawing stage was
measured a frame after it was drawn. Measured before the first paint now.
---
## 2026-08-16 — Marks, lines, and a table that breaks
**Images** — web `sha256:1f9f5141…`, server `sha256:3b2f8b44…`
The server gained `icon` on notes and chapters, `icon_color` on notes, and colour
and style on separators.
- **A note and a chapter can wear a mark**, chosen from a short list searched by
what a thing is *for* — "invoice", "house", "travel" — rather than from the
system emoji keyboard, which is a different thing on every machine.
- **A separator takes its own colour and one of five shapes.** Three faint grey
lines through one notebook read as the same line repeated.
- **A long table lay across the gap between two sheets** with a row cut in half.
Tables are broken between their rows now, which is where a table breaks on
paper; the spacer is a `tr`, since a `div` inside a `tbody` is not something the
browser keeps.
- **A dialog closed in the middle of an edit** when a selection was dragged past
its edge: the click lands on the nearest common ancestor of press and release.
All ten of them remember where the press began.
- **A chapter's colour could not be cleared** — "None" sent null, and the update
rode on `COALESCE`, which reads null as "leave it alone".
- A table's header row is legible on the dark theme, and opening a notebook opens
its first note on a wide screen.
---
## 2026-08-12 — Chapters, and a phone that works
**Images** — web `sha256:c077f0e0…`, server `sha256:c13af5d5…`
(earlier that day: web `65799ecb…`, `2041f103…`, `95657aaf…`, `3700247c…`,
`790ff73a…`, `1217b81b…`, `5ea846f3…`, `c1222665…`; server `a51b96ad…`,
`ed0b9970…`, `b4db0725…`, `181d77be…`, `3303d3dc…`, `06f94f43…`)
The server gained a `chapters` table, `chapter_id` on notes, a `color` column and
a reorder route for chapters, named positions on the note reorder, a
`cover_pattern` column, and the per-account setting for how a chapter opens.
- **Chapters — folders inside a notebook.** A note is in one or in none. Removing
a chapter never removes what it held, and the dialog says so with the number
affected. They sit at the same level as notes and separators: one counter, one
grip, one kind of drag.
- **Two ways for a chapter to open**, chosen per account: a further column beside
the list, or unfolded under its own row. A phone fills the list instead.
- **A chapter is made complete in one dialog** — name and colour together — and a
chapter with no notes in it is visible at last; "nothing here" had been asked of
the notes alone.
- **A notebook starts in the hand-made order**, which is where its arrangement
lives. The views across all notebooks keep "last changed": they have nowhere to
keep an arrangement. **Rearranging is a mode**, switched on beside the sort menu.
- **A filed note came back out of its chapter by itself.** The server had it right
the whole time; the sync that follows rebuilds every note from the server's
answer, and the function doing that never carried `chapter_id`.
- **Removing a chapter left you stuck in the confirmation** — that one path set
the dialog directly instead of going through the helper that takes it down. The
suites had read the list straight through the open overlay and passed; they now
assert the question is gone.
- **The order of notes could not be changed on a phone**: the list used the HTML5
drag API, which mobile browsers never fire from touch. Pointer events and a grip
per row. **"Move to…" opened off the side of the screen** and reacted only to
hover; it opens on a tap and flips side when there is no room.
- **A note made offline lost everything written into it** — its create reached the
server, the server named it something else, and every queued edit went on
addressing the name only that device knew. The queue is repointed with the
database now.
- **The install button never appeared, even over HTTPS.** `beforeinstallprompt`
fires before React mounts, so it is caught in `index.html`; and the manifest is
fetched without credentials unless the link says `use-credentials`, so behind an
authenticating proxy the browser read the login page as the manifest.
- **The service worker could cache a proxy's login page as the application** and
then "work offline" by showing a login screen it could never get past.
Redirected and unsuccessful responses are no longer cached.
- **A cover is a colour and a pattern, chosen in turn** — every combination, drawn
in CSS, with existing notebooks keeping exactly the cover they had.
- The search box in a notebook is simply there, the margin-note column is half
again as wide, and a submenu is no longer clipped away on a wide screen.
---
## 2026-08-11 — Writing on paper
**Images** — web `sha256:3a181ff3…`, server `sha256:0832ee2b…`
(earlier that day: web `b764a9d8…`, `dc804f7b…`, `ed11f034…`, `3b040d99…`;
server `1648498f…`)
The server gained a column and two routes for extracted PDF text.
- **You write on paper.** Each page is a sheet of its own, centred, with a gap
after it, and the text is broken against those sheets so no paragraph is cut in
half. The breaks are decorations: nothing enters the document, reaches the
server, or appears in an export. The toolbar says *Page 2 of 4*.
- **A drawing runs over as many pages as the note has**, numbered the way the PDF
reader numbers them, with one blank page past the end to start on. A drawing
that lives on page one alone is still stored in the old shape.
- **A PDF is searchable on every device**, not only the one that imported it: the
importing device extracts the text and stores it beside the file, encrypted. The
server still has no PDF library.
- **The typeface is a per-account setting** — serif, sans or mono — so it follows
you to the phone. **Text alignment** likewise, with its own extension so the
schema stays in files this project owns; non-left blocks travel as
`<div align="…">` in Markdown.
- **A note with a drawing could not be typed in at all**: the box wrapping each
page's ink layer lay over the text and swallowed every click.
- **A drawing exported in a ZIP was lost on re-import** — the exporter had written
`drawings/…` since the interface went English, the importer still looked for
`zeichnungen/…`.
- **"Start writing..." was printed at the foot of every page** with a break after
it: the placeholder styles `[contenteditable]:empty::before`, and the spacer
holding a gap open is exactly that.
- The Markdown exporter began putting a blank line between top-level blocks, and
the margins narrowed from 73 to about 80 characters.
---
## 2026-08-10 — Notes on passages
- **Pinned notes in a PDF.** A remark is attached to a passage and shown in the
margin beside the page, never written on top of it; on a phone it opens as a
bottom sheet. Such a remark can be linked from the notes panel and written about
at length.
- The server stored an overlay holding only pinned notes as `NULL`, throwing every
one of them away, because it counted strokes and nothing else.
---
## 2026-08-09 — Arranging a notebook
- **Sorting per notebook**, remembered on the notebook, including an arrangement
made by hand — which comes back intact after filtering.
- **Separators**, **Markdown import** (single files or a ZIP this app exported),
and **moving notes between notebooks**.
- Switching to Draw shrank the page from 733px to 460px: the editor was a flex
item at `flex: 0 1 auto`, so it was only as wide as its widest row — the
toolbar, which loses a dozen buttons in Draw.
---
## 2026-08-08 — PDFs
- Import a PDF, read it, select its text, draw on each page, and export it with
the drawings written in. Notes written beside a PDF.
- Uploaded files are encrypted at rest, like note bodies already were, and stored
as deduplicated, content-addressed attachments; unused ones are freed, and
versions are kept by count rather than by age.
- Export as Markdown or PDF; the browser suites moved into the repository.
- Vector clocks were never read — replaced with a conflict check that works.
- Public sign-up removed: an administrator creates the accounts.
---
## Earlier
The interface became English throughout, and search inside a notebook replaced
the sidebar's global search (2026-08-11). Before that: the TipTap editor, version
history, full-text search and attachments (2026-08-07); drawing sync with
WebP/JPEG support (2026-06-21); the first syncing alpha and PWA (2026-02-18).