Notes module
A private dated notebook opened from the brand wheel in the shell. For two named people — a place for James's day-notes he was writing in Apple's Notes and emailing across, now in the Hub itself.
| Folder | src/modules/notes/ |
| Opens from | The brand wheel in the shell (the dev overlay this replaced is gone — 242b17a) |
| Migrations | 127 (shop_notes + the two-person allowlist), 128 (shelved by tab), 129 (note_images — inline pictures) |
| Storage | Base64 in a TEXT column, same shape as announcement_images on the Shop Board |
The gate is an identity check, NOT a permission (mig 127)
⚠ core/perms.ts grants sys_admin the entire permission catalog by construction, so perms.can("screen.notes") would resolve true for every admin account this database has or will ever have — including staff 1001, the automation account. The notebook is for two named people, so the check is by staff id against a column seeded for exactly those two (mig 127).
The gate is checked server-side, on every route. The wheel affordance in the shell is also gated — but that only decides whether a control is drawn; anyone can POST to a URL, so hiding a button is never the control.
(Hiding a control is never the control — the client can hide a button but cannot enforce that no client posts to the URL; the server is where the gate belongs. Repeated here because the primary gate is an allowlist, not a permission, and future readers need to know it.)
Shelved by tab — the rail is the TAB STRIP, not dates (mig 128)
The first cut filed notes by date, which made the notebook a diary: a thought about Sales sat next to a thought about Rentals because they happened the same afternoon. That is the wrong axis. James does not reach for "what did I write on Tuesday", he reaches for "what do I need to say about Sales" — so the tab is the shelf and dates sort inside, newest at the top.
The rail is rendered server-side from the registry's own tab list, in strip order, so it is the same list in the same order as the nav above it and cannot drift from it. A module registered tomorrow gets a shelf in the notebook without this file being touched. There is also a general shelf for notes about the shop rather than a specific screen.
Pictures — paste, drag or pick (mig 129)
"Rich text box with inline image ability" (Tom). Same shape as announcement_images (the Shop Board has used it for inline pictures since it shipped). A second mechanism for the same job would be a second thing to get wrong.
- Base64 in a TEXT column (
note_images). - Served back from our own route — never a data-URI in the note itself.
- The sanitiser accepts an
<img>only when itssrcis exactly that route — nodata:URIs, no third-party image hosts, no chance of a paste smuggling an off-domain fetch into the page. - Three input paths: paste an image from the clipboard, drag it onto the editor, or pick via file input.
The editor
A contenteditable div driven by document.execCommand. That API is deprecated on paper and implemented everywhere, because the web is full of editors built on it. The alternative is a selection-and-range engine of our own, or pulling in a library once the CSP work lands. If execCommand ever does break, it breaks into "the toolbar buttons stop working" while typing still works — which is the graceful failure mode this module lives with by choice.
The modal is wider than the default
The Notes modal rides the shared .kb-modal chassis, but overridden here and nowhere else — the shared .kb-modal-card's 540px stays 540px for every other confirm dialog. A confirm dialog is a surface to answer yes/no; a notebook is a surface to write on.
What this module deliberately does NOT do
- Not a shared editing surface — dated entries, newest first, one author per entry. Two people editing one paragraph is a merge problem nobody asked for.
- Not tied to any shop record — a note about a customer, ticket, or bike is not a comment ON that record; it lives in the notebook alongside every other thought.
- No sharing outside the two-person allowlist — there is no public URL, no export, no message-to-Slack; the notes exist only in the Hub for the two people the allowlist names.
See also
- My Hub — Shop Board — the public-shop-facing analogue (announcements the whole shop sees); shares the
announcement_imagesstorage pattern - Audit everything — every note write is audited, same as any other module mutation