From a27d5de9c78e71288985322747f0f65b52a7aea5 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Thu, 22 Oct 2026 20:10:13 +0200 Subject: [PATCH] phone app: installable, opens offline, takes the share sheet onto the scratchpad, back lets go of one thing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A manifest with a 192, a 512 and a maskable icon, and a hand-written service worker built beside the app by a small vite plugin: it keeps index.html, the logo, the entry with its imports and the latin fonts, keeps each hashed asset once fetched, asks the server for pages first and answers from the cache when it is down, and never touches /api. The CSP names worker-src and manifest-src. The share target posts to /share, which the worker catches: the text waits in a cache and becomes a scratchpad note once someone is logged in, with the drawer down on it. On a touch screen one guard entry in the history makes back let go of one thing at a time — a prompt, the line, a sheet, the drawer, the editor, a tile, then a folder up — and leave only at a space's root. The bar and the band grow by the safe-area insets. The login no longer says the server is unreachable once it answers again with the session gone. --- PLAN.md | 33 +++++++- README.md | 25 +++++- docs/wiki/Capture.md | 5 ++ docs/wiki/Getting-Started.md | 3 + docs/wiki/Guide.md | 16 +++- src/main.rs | 5 +- web/index.html | 3 +- web/public/icon-192.png | Bin 0 -> 9956 bytes web/public/icon-512.png | Bin 0 -> 35633 bytes web/public/icon-maskable-512.png | Bin 0 -> 15378 bytes web/public/manifest.webmanifest | 22 ++++++ web/src/App.svelte | 33 +++++++- web/src/app.css | 17 ++-- web/src/lib/DocumentPane.svelte | 2 +- web/src/lib/StatusBar.svelte | 2 +- web/src/lib/actions.ts | 2 +- web/src/lib/modes.ts | 32 +++++++- web/src/lib/state/scratch.svelte.ts | 22 ++++++ web/src/lib/state/tree.svelte.ts | 4 +- web/src/lib/store.svelte.ts | 6 ++ web/src/main.ts | 4 + web/src/sw.ts | 117 ++++++++++++++++++++++++++++ web/vite.config.ts | 50 +++++++++++- 23 files changed, 377 insertions(+), 26 deletions(-) create mode 100644 web/public/icon-192.png create mode 100644 web/public/icon-512.png create mode 100644 web/public/icon-maskable-512.png create mode 100644 web/public/manifest.webmanifest create mode 100644 web/src/sw.ts diff --git a/PLAN.md b/PLAN.md index 06d89e0..478c634 100644 --- a/PLAN.md +++ b/PLAN.md @@ -47,6 +47,30 @@ them, in one commit: - `o` in a space with templates offers them on the line, `:new `; `Enter` on nothing is still a blank document. +## The phone app, done + +Written on 2026-10-22. The workspace installs on a phone as an app: + +- A manifest (`web/public/manifest.webmanifest`) with a 192 and a 512 icon and + a maskable one, drawn from the favicon. +- A service worker (`web/src/sw.ts`, built by a small plugin in + `vite.config.ts`) keeps index.html, the logo, the entry with what it imports, + and the latin fonts, and every hashed asset once it has been fetched. Pages + are asked of the server first and answered from the cache when it is down. + It never touches `/api`. The CSP names `worker-src` and `manifest-src`. +- A share target, `POST /share`, caught by the worker and never sent to the + server: the text waits in the cache `share` and becomes a scratchpad note + once someone is logged in. +- Back, on a touch screen, lets go of one thing at a time through one guard + entry in the history (`back()` in `modes.ts`), and leaves the app at the + root of a space. +- The bar and the band grow by the safe-area insets, so an installed app keeps + them clear of the status bar and the gesture bar. + +Not done, on purpose: reading documents offline, and sharing a photo or a file +— a file needs a document or a folder to belong to, and a note has neither. +iOS has no share target, so the iPhone keeps the Shortcut. + ## Fixed before, on 2026-10-21 After the seven slices of the feature plan (trash `8ffad63`, revisions @@ -83,9 +107,10 @@ tab. `.github/workflows/wiki.yml` replaces it on its first run. The backlog, fitted to the model rather than given new surfaces: -- Install the app on a phone (a PWA: manifest plus a service worker for the - app shell, CSP `worker-src 'self'`), with a share target that posts into - capture as a note. The touch and desktop menus are already the same. +- Read on a phone with no network: the documents opened last, kept by the + service worker or in IndexedDB, and cleared on logout. +- Share a photo or a file into the app: it asks which document or folder it + belongs to. - Task lists: toggle `- [ ]` in the reader, and the open ones of a space as a section of the agenda rather than a `:tasks` command. - A daily review, also as a section of the agenda: yesterday's entry, what was @@ -95,4 +120,4 @@ The backlog, fitted to the model rather than given new surfaces: Dropped: marks (`ma` … `mz`). The info sheet's headings and the tiles cover what they were for, and `m` stays picking nodes up. -Suggested next: the PWA. +Suggested next: task lists and the daily review, as sections of the agenda. diff --git a/README.md b/README.md index f556ffb..45a9569 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ document per day. A file sits in a folder or belongs to its document. card, or more of a document. - **Capture.** A token per phone or script that puts a card in an inbox, a note on the scratchpad or a line in today's entry, and nothing else. +- **Install.** On a phone the workspace installs as an app: it opens with no + network, sits in the share sheet, where whatever is shared to it lands on + the scratchpad, and back closes one thing at a time. - **Share.** Any document behind an unguessable, revocable read-only link. - **Mount.** The whole workspace over SFTP, for an editor on another machine. - **Back up.** A mirror to Proton Drive, end-to-end encrypted, as a registered @@ -223,6 +226,18 @@ has today's entry, an upload into the folder, the trash, a search across every s and on the board the card moves, renames and deletes; the agenda has a done button per card. +Over HTTPS the browser offers to install the workspace (Chrome's menu: *Install +app*, or *Add to Home screen*). Installed, it opens in a window of its own, from +its own icon, and with no network it still opens, to say the server cannot be +reached and try again. Back — the gesture or the button — lets go of one thing +at a time, the way `Esc` does: a sheet, the scratchpad, the editor, the +document, then a folder up; at the root of a space it leaves the app. On +Android the installed app is in the share sheet: a link or some text shared to +it becomes a note on the scratchpad, the title on its first line, and waits +there through a login if the session has run out. A service worker keeps the +app's own files and nothing else; it never stores a document, a card or a +note. + ## Tiles Several documents can be open at once. `L` (or `Shift-Enter`) on a row opens @@ -416,7 +431,8 @@ On an iPhone, a Shortcut with *Ask for Input* followed by *Get Contents of URL* does it: method `POST`, a header `Authorization` of `Bearer nwc_…`, and a JSON body with `text` set to the input. On Android, *HTTP Shortcuts* does the same with a text-input variable in the body. Either one goes on the home -screen, or behind the share sheet. +screen, or behind the share sheet. On Android the installed app is in the share +sheet itself, with no token, and puts what is shared on the scratchpad. ## Not losing things @@ -951,7 +967,9 @@ sealed or not. ## Deploying Put a TLS-terminating reverse proxy in front of it, point it at `WORKSPACE_BIND`, -and set `WORKSPACE_SECURE_COOKIE=1`. Nothing else is needed: no database server, no +and set `WORKSPACE_SECURE_COOKIE=1`. HTTPS is also what lets a phone install the +app: a browser runs a service worker only over HTTPS, or on `localhost`. +Nothing else is needed: no database server, no runtime dependencies, no build tools on the host. The server will not start while `WORKSPACE_SECRET` or `WORKSPACE_PASSWORD` is still the example from `.env.example`: with the example secret anyone could sign themselves a session. @@ -972,7 +990,8 @@ everyone out once. The app sends its own `Content-Security-Policy`, `Referrer-Policy: no-referrer`, `X-Content-Type-Options`, `X-Frame-Options`, `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy` and a `Permissions-Policy` that turns off camera, -microphone and location; API answers are `Cache-Control: no-store` unless they +microphone and location. The policy lets in one worker and one manifest, the +app's own (`/sw.js`, `/manifest.webmanifest`); API answers are `Cache-Control: no-store` unless they say otherwise. A proxy that adds its own should not weaken them. `no-referrer` matters in particular, because a share token lives in the URL and would otherwise leak to any site a shared entry links to. Share links minted before the key existed are revoked by migration `0005`; diff --git a/docs/wiki/Capture.md b/docs/wiki/Capture.md index eedac4f..c5b703e 100644 --- a/docs/wiki/Capture.md +++ b/docs/wiki/Capture.md @@ -60,3 +60,8 @@ with a text-input variable, method `POST`, the same header, and a JSON body `{"text": "{{input}}"}`. Either one can sit on the home screen, or behind the share sheet. + +**Android, without a token**: install the workspace from Chrome (*Install app*, +over HTTPS) and it is in the share sheet itself. What is shared to it — a link, +some text, a title — becomes a note on the scratchpad, from the session the app +is logged in with. See [On a phone](Guide.md#on-a-phone). diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index d40ec4a..8fd223f 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -80,6 +80,9 @@ The rail of buttons on the right does the same as the keys, for a mouse. On a phone the same things are buttons: the dock at the bottom has a new document, a new folder, the board, today's entry, an upload and a search, and the `⋯` on a row (or a long press on it) has what can be done to that row. +Over HTTPS the browser offers to install it as an app, which then sits in the +share sheet and puts what is shared on the scratchpad; see +[On a phone](Guide.md#on-a-phone). ![The workspace on a phone](../images/mobile-tree.png) diff --git a/docs/wiki/Guide.md b/docs/wiki/Guide.md index 6216832..6573a72 100644 --- a/docs/wiki/Guide.md +++ b/docs/wiki/Guide.md @@ -205,7 +205,9 @@ curl -H "Authorization: Bearer nwc_…" -H 'content-type: application/json' \ -d '{"text":"call the bank"}' https://your-host/api/capture ``` -The token can do this and nothing else. See [Capture](Capture.md). +The token can do this and nothing else. On Android the installed app needs no +token: share to it, and the text is a note on the scratchpad. See +[Capture](Capture.md) and [On a phone](#on-a-phone). ## Evening: the diary @@ -251,3 +253,15 @@ has a done button on each card. | ![The tree on a phone](../images/mobile-tree.png) | ![A document on a phone](../images/mobile-document.png) | `:set vim` turns the vim keys on for a phone with a keyboard. + +Over HTTPS the browser offers to install the workspace as an app (Chrome's +menu: *Install app*, or *Add to Home screen*). Installed, it opens from its own +icon, and with no network it still opens, to say the server cannot be reached. +Back lets go of one thing at a time, as `Esc` does — a sheet, the scratchpad, +the editor, the document, then a folder up — and only at the root of a space +leaves the app. + +On Android the installed app is in the share sheet. A link or some text shared +to it becomes a note on the scratchpad, the title on its first line; the drawer +comes down with it on top, ready to be kept, made a card or put at the end of a +document. A share made while logged out waits for the login. diff --git a/src/main.rs b/src/main.rs index ceb476f..ab85c2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -406,9 +406,12 @@ fn compressible( /// Content-Security-Policy for the app shell. `unsafe-inline` is granted to /// styles only, because CodeMirror injects its theme as a