# 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-25 — The bar that ties the apps together **Images** — web `sha256:7c802a96…`, server `sha256:a156fb92…` **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. --- ## 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 `