Choose a cover in two parts, and let the app be installed
A cover is a colour *and* a pattern now, chosen in turn. Offering every combination as its own entry meant fifteen colours times a dozen patterns in a grid nobody could take in — and in practice only the handful someone had thought to write out. Each pattern is previewed over the colour in hand, so what is on screen is the cover as it will be. `cover_pattern` is its own column: they are two decisions, and folding them into one number would mean renumbering every cover the day a pattern is added. For one day the patterns *were* twelve appended entries. Those indices are read back as the colour-and-pattern pair they were drawn as, so nothing chosen then looks different now. And three separate reasons the install button never appeared, none of them in anyone's configuration: - `beforeinstallprompt` fires once, early — usually before React has mounted — and is never fired again. A listener added from inside a component simply missed it. It is caught at the top of index.html now. - A `<link rel="manifest">` is fetched with credentials omitted unless it says otherwise. Behind a reverse proxy that authenticates in front of the app, that one request comes back as the proxy's login page and the browser reads it as an invalid manifest — while the app itself works perfectly, because its own requests carry the cookie. - The service worker cached whatever came back, so a lapsed session could be stored as the application itself. The app would then "work offline" by showing a login screen it could never get past, curable only by clearing the site's data. Only successful, unredirected responses are cached now, and the cache name is bumped to drop anything already stored under the old rules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d8d1180de
commit
8ca1ed6e06
17 files changed
+455
-221
No files matched your search
@@ -347,31 +347,65 @@ The install entry used to appear only when the browser had offered a prompt
|
||||
screen at all — no button, no reason, and no way to find out short of knowing how
|
||||
PWAs work.
|
||||
|
||||
It is always there now, and when there is no prompt it says which of the fixable
|
||||
causes applies. The common one is worth naming: **a browser will only install a
|
||||
**The prompt itself was being missed.** `beforeinstallprompt` fires once, early
|
||||
— usually before React has mounted — and is never fired again. The listener lived
|
||||
in a component, so on a correctly served site the event arrived, found nobody
|
||||
listening, and was gone for the session. It is caught at the top of `index.html`
|
||||
now and handed over when the app is ready.
|
||||
|
||||
**And the manifest was being fetched without credentials.** A
|
||||
`<link rel="manifest">` is fetched with credentials omitted unless it says
|
||||
otherwise, so behind a reverse proxy that authenticates in front of the app that
|
||||
one request comes back as the proxy's login page. The browser reads it as an
|
||||
invalid manifest and offers no install — while the app itself works perfectly,
|
||||
because its own requests carry the cookie. The link now carries
|
||||
`crossorigin="use-credentials"`, which is harmless without a proxy.
|
||||
|
||||
The service worker had the matching problem: it cached whatever came back. A
|
||||
lapsed session could be stored as the application itself, and the app would then
|
||||
"work offline" by showing a login screen it could never get past — curable only
|
||||
by clearing the site's data. It now caches only responses that are successful and
|
||||
not redirected, and `Cache.put` is guarded, since it rejects on a redirect and
|
||||
that rejection was going unhandled on every navigation.
|
||||
|
||||
Behind such a proxy, three paths have to reach InkNote intact: `/manifest.json`
|
||||
and `/sw.js` must return the file rather than a login page — a **redirect** on
|
||||
`sw.js` fails registration outright, taking offline with it — and `/ws` needs its
|
||||
WebSocket upgrade passed through, or changes from another device only arrive with
|
||||
the 30-second reconciliation.
|
||||
|
||||
The entry is always there, and when there is no prompt it says which of the
|
||||
fixable causes applies. The common one is worth naming: **a browser will only install a
|
||||
site served over HTTPS**, and on plain HTTP the service worker never registers
|
||||
either — so one cause takes the install *and* offline with it, which otherwise
|
||||
looks like two unrelated mysteries.
|
||||
|
||||
### Notebook covers
|
||||
|
||||
A cover is stored as an **index** into `COVERS` in
|
||||
[`ui/constants.jsx`](frontend/src/ui/constants.jsx). Entries are therefore only
|
||||
ever appended, never reordered: renumbering them would silently repaint every
|
||||
notebook anyone already has.
|
||||
**A cover is a colour and a pattern, chosen in turn** — `COVERS` and `PATTERNS`
|
||||
in [`ui/constants.jsx`](frontend/src/ui/constants.jsx), stored as
|
||||
`notebooks.cover_index` and `notebooks.cover_pattern`.
|
||||
|
||||
The patterned ones are drawn entirely with CSS gradient layers — no image files,
|
||||
nothing to upload, nothing to fetch. They stay crisp at any size and work offline
|
||||
like the rest of the app. `background` takes a stack of layers, topmost first, so
|
||||
each is a pattern laid over the colour beneath it.
|
||||
Two decisions rather than one list. Offering every combination as its own entry
|
||||
would mean fifteen colours times a dozen patterns in a grid nobody could take in
|
||||
— and in practice only the handful someone had thought to write out. Picking each
|
||||
in turn is shorter *and* gives every combination. Each pattern is previewed over
|
||||
the colour currently chosen, so what is on screen is the cover as it will be.
|
||||
|
||||
They are deliberately faint: a cover is read at the size of a thumb with a title
|
||||
across it, and anything louder fights the title. One idea per cover, too — two
|
||||
crossed diagonals meant as a chevron came out as noisy plaid at that size and
|
||||
were replaced with plain bands.
|
||||
The patterns are CSS gradient layers laid over the colour — no image files,
|
||||
nothing to upload, nothing to fetch, crisp at any size, offline like the rest of
|
||||
the app. They are deliberately faint: a cover is read at the size of a thumb with
|
||||
a title across it, and anything louder fights the title. One idea each, too — two
|
||||
crossed diagonals meant as a chevron came out as noisy plaid at that size.
|
||||
|
||||
The grid scrolls. Without that the dialog runs past the bottom of a laptop screen
|
||||
and takes the *Create* button with it, which a test now checks at 1440×800.
|
||||
`COVERS` is an index, so entries are only ever appended: renumbering them would
|
||||
silently repaint every notebook anyone already has. For one day the patterns
|
||||
*were* twelve appended entries, before they became their own axis; `coverParts()`
|
||||
reads those indices back as the colour-and-pattern pair they were drawn as, so
|
||||
nothing that was chosen then looks different now.
|
||||
|
||||
Both grids scroll. Without that the dialog runs past the bottom of a laptop
|
||||
screen and takes the *Create* button with it, which a test checks at 1440×800.
|
||||
|
||||
### Browser suites
|
||||
|
||||
|
||||
Reference in new issue
Block a user