The password tab was hidden for accounts with no password, which is not the same as accounts that sign in through the provider — an account linked to one kept its password and therefore kept the tab. That is precisely the account the person setting this up is using. It goes by "signs in over there" now; the password it kept is the emergency door, set by an administrator. And the picture. The provider sends one, the server fetches it at each sign-in and stores it encrypted in a table beside the users rather than in them, and serves it back through the API — so the page never talks to the provider, no third-party cookie is needed for a face to appear, and it survives the provider being down like everything else here. Fetched once per account into an object URL, since an img tag cannot carry a bearer token. The suite's provider now serves a picture too, and the check asks the browser whether it actually decoded rather than whether the element exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
333 lines
18 KiB
Markdown
333 lines
18 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-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).
|