diff --git a/.env.example b/.env.example
index da2c84c..ace8a75 100644
--- a/.env.example
+++ b/.env.example
@@ -101,6 +101,8 @@ WORKSPACE_DIGEST_AT=sun 18:00
# Which of the four kinds may be sent. Drop one from the list to switch it off.
WORKSPACE_MAIL_KINDS=reminder,card_due,digest,login
+# --- Vault, trash and history ------------------------------------------------
+
# Minutes the vault stays open with nothing using it, before it locks itself.
# It also locks on logout and whenever the server restarts.
WORKSPACE_VAULT_IDLE_MIN=15
diff --git a/Makefile b/Makefile
index 06a0562..f031bf3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: dev dev-api dev-web build run check clean
+.PHONY: dev dev-api dev-web build run check wiki screenshots clean
# Two terminals: `make dev-api` and `make dev-web` (http://127.0.0.1:4243).
dev-api:
@@ -20,6 +20,17 @@ check:
cargo clippy --all-targets -- -D warnings
cargo test
cd web && bun run check
+ bun scripts/wiki.ts --check
+
+# The generated wiki pages, from the code they describe: see docs/wiki/Development.md.
+wiki:
+ bun scripts/wiki.ts
+
+# docs/images, taken again from a throwaway workspace in a headless Chromium.
+screenshots:
+ cd web && bun install --frozen-lockfile && bun run build
+ cargo build
+ bun scripts/screenshots.ts
clean:
cargo clean
diff --git a/README.md b/README.md
index a69107a..5b39c48 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
# ~/workspace
A private, terminal-themed workspace that runs in the browser: a diary, the
-notes for work and for uni, and a board per space. One user, one SQLite file,
-one binary. It was called `narl-diary` until it grew everything but the diary.
+notes for work and for uni, a board per space, and the files that go with them.
+One user, one SQLite file, one binary. It was called `narl-diary` until it grew
+everything but the diary.
-- **Rust + axum** backend, **SQLite** storage, **Svelte 5** frontend.
-- Spaces (`diary`, `work`, `uni`, …) with folders nested to any depth inside them.
-- Markdown documents with full-text search across a whole space (SQLite FTS5),
- reaching into the text files in the tree too,
- and math in them set by KaTeX: `$x^2$` inline, `$$…$$` or a ```` ```math ````
- block on its own.
-- Import a folder — or a zip of one — as a tree of documents, links and all.
-- A kanban board per space, so what is open at work is not mixed up with uni.
-- Drag, drop or paste images, video and audio straight into an entry.
-- Files of any kind — PDFs, archives, recordings — live in the tree beside the
- notes about them, streamed to disk however large — and PDFs, text, code,
- tables and certificates open in place.
-- Share any document behind an unguessable link — revocable at any time.
-- A vault: files sealed with AES-256-GCM under a password that is never stored,
- readable only while it is unlocked.
-- Backs itself up to Proton Drive, end-to-end encrypted, as a registered device.
-- Mails you — a reminder when the diary is empty, due cards, a weekly digest.
-- Real vim keybindings on the desktop (CodeMirror 6 + vim mode), plain taps on
- mobile.
-- The whole frontend is embedded in the release binary, so deploying is copying
- one file.
+
+
+**New here?** The [guide](docs/wiki/Guide.md) walks through a day with it, and
+[getting started](docs/wiki/Getting-Started.md) has it running in five minutes.
+Everything is also in the [wiki](https://github.com/narrrl/narl-workspace/wiki).
+
+## What it does
+
+- **Write.** Markdown documents in spaces (`diary`, `work`, `uni`, …) with
+ folders to any depth. Real vim keys on the desktop (CodeMirror 6), plain taps
+ on a phone. Math by KaTeX, `$x^2$` and `$$…$$`. `[[Title]]` links that are
+ rewritten when what they point at is renamed or moved, and backlinks.
+- **Plan.** A kanban board per space, due dates said the way you say them
+ (`fri`, `+3d`), cards that come back once done, and an agenda of what is late,
+ due or in hand on every board.
+- **Remember.** A diary with a calendar, templates, the entries from this day
+ in earlier years, and an evening reminder when nothing was written.
+- **Keep files.** PDFs, archives and recordings live in the tree beside the
+ notes about them, streamed to disk however large. PDFs, text, code, tables
+ and certificates open in place, and search reads the text files too.
+- **Find.** Full-text search across a space or all of them (SQLite FTS5).
+- **Lose nothing.** Autosave that survives a dropped network, every earlier
+ version of a document as a diff, a trash with undo, an export that reads
+ without this app, and an import that takes it back.
+- **Seal.** A vault: files sealed with AES-256-GCM under a password that is
+ never stored.
+- **Capture.** A token per phone or script that puts a card in an inbox or a
+ line in today's entry, and nothing else.
+- **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
+ device, and mail for reminders, due cards and a weekly digest.
+- **Deploy.** Rust and axum on the server, Svelte 5 in the browser, and the
+ whole frontend embedded in the release binary, so deploying is copying one
+ file.
+
+| | |
+| --- | --- |
+|  |  |
+|  |  |
+|  |  |
## Quick start
@@ -59,7 +91,15 @@ make dev-web # vite dev server on :4243, proxying /api to :4242
```
In debug builds the binary reads `web/dist` from disk, so `cargo run` alone also
-works once the frontend has been built at least once.
+works once the frontend has been built at least once — and not before: the
+binary embeds `web/dist`, so a fresh clone builds the frontend first.
+
+`make check` runs what CI runs: format, clippy, the tests, svelte-check, and a
+check that the generated wiki pages are current. The wiki is written in
+`docs/wiki` and published by CI on every push to `main`; `make wiki`
+regenerates the pages read out of the code, and `make screenshots` takes the
+pictures in `docs/images` again. See
+[Development](docs/wiki/Development.md).
## Keys
diff --git a/docs/images/agenda.png b/docs/images/agenda.png
new file mode 100644
index 0000000..99680de
Binary files /dev/null and b/docs/images/agenda.png differ
diff --git a/docs/images/board.png b/docs/images/board.png
new file mode 100644
index 0000000..6437be1
Binary files /dev/null and b/docs/images/board.png differ
diff --git a/docs/images/calendar.png b/docs/images/calendar.png
new file mode 100644
index 0000000..8bd439d
Binary files /dev/null and b/docs/images/calendar.png differ
diff --git a/docs/images/document.png b/docs/images/document.png
new file mode 100644
index 0000000..72d1ce2
Binary files /dev/null and b/docs/images/document.png differ
diff --git a/docs/images/editor.png b/docs/images/editor.png
new file mode 100644
index 0000000..2d4fc07
Binary files /dev/null and b/docs/images/editor.png differ
diff --git a/docs/images/file-certificate.png b/docs/images/file-certificate.png
new file mode 100644
index 0000000..cd8b8a3
Binary files /dev/null and b/docs/images/file-certificate.png differ
diff --git a/docs/images/file-table.png b/docs/images/file-table.png
new file mode 100644
index 0000000..c3841ec
Binary files /dev/null and b/docs/images/file-table.png differ
diff --git a/docs/images/help.png b/docs/images/help.png
new file mode 100644
index 0000000..24d0ac2
Binary files /dev/null and b/docs/images/help.png differ
diff --git a/docs/images/history.png b/docs/images/history.png
new file mode 100644
index 0000000..d1e4816
Binary files /dev/null and b/docs/images/history.png differ
diff --git a/docs/images/login.png b/docs/images/login.png
new file mode 100644
index 0000000..37a42d3
Binary files /dev/null and b/docs/images/login.png differ
diff --git a/docs/images/math.png b/docs/images/math.png
new file mode 100644
index 0000000..f6b84bd
Binary files /dev/null and b/docs/images/math.png differ
diff --git a/docs/images/mobile-document.png b/docs/images/mobile-document.png
new file mode 100644
index 0000000..6739841
Binary files /dev/null and b/docs/images/mobile-document.png differ
diff --git a/docs/images/mobile-tree.png b/docs/images/mobile-tree.png
new file mode 100644
index 0000000..1244cdb
Binary files /dev/null and b/docs/images/mobile-tree.png differ
diff --git a/docs/images/overview.png b/docs/images/overview.png
new file mode 100644
index 0000000..6330f8c
Binary files /dev/null and b/docs/images/overview.png differ
diff --git a/docs/images/search.png b/docs/images/search.png
new file mode 100644
index 0000000..aa851d9
Binary files /dev/null and b/docs/images/search.png differ
diff --git a/docs/images/tiles.png b/docs/images/tiles.png
new file mode 100644
index 0000000..9a0c5f4
Binary files /dev/null and b/docs/images/tiles.png differ
diff --git a/docs/wiki/API.md b/docs/wiki/API.md
new file mode 100644
index 0000000..b28c148
--- /dev/null
+++ b/docs/wiki/API.md
@@ -0,0 +1,59 @@
+
+
+# API
+
+Everything except the login, the health check and the two share routes
+requires the session cookie; `/api/capture` takes a token instead. Media answers `Range` requests, so a video seeks,
+sealed or not.
+
+| method | path | purpose |
+| --- | --- | --- |
+| `GET` | `/api/health` | public: up, and the database answers — for a supervisor |
+| `POST` | `/api/login`, `/api/logout` | session; `/api/logout?everywhere=true` ends every one |
+| `GET` | `/api/me` | current user |
+| `GET` | `/api/sessions` | every signed-in browser, the one seen last first |
+| `DELETE` | `/api/sessions/{id}` | sign another browser out, and lock the vault |
+| `GET`/`POST` | `/api/spaces` | the spaces / create one |
+| `GET`/`POST` | `/api/nodes` | children (`?parent=`) or search (`?q=`, `?space=`; a file matched by its text has `in_text` and the words around the match as `excerpt`) / create, from a template with `template_id` |
+| `GET`/`PUT`/`DELETE` | `/api/nodes/{id}` | read / update / delete into the trash, subtree and all — or for good, for something already in it |
+| `GET`/`DELETE` | `/api/trash` | what was deleted, the last first / empty it |
+| `DELETE` | `/api/trash/{id}` | delete one thing in the trash for good |
+| `POST` | `/api/trash/{id}/restore` | back where it was deleted from, or to its space when that folder is gone |
+| `GET` | `/api/spaces/{id}/board` | the lists and cards of one space |
+| `GET` | `/api/agenda` | open cards due within a week or in hand, on every board, whether the diary has today, and its entries `on_this_day` in earlier years |
+| `GET` | `/api/diary/days?month=2026-10` | the diary's entries in a month, each with its local `day` |
+| `GET` | `/api/diary/around/{id}` | the entries before and after a diary entry, by date |
+| `GET` | `/api/nodes/{id}/templates` | the templates of the space the node is in |
+| `POST` | `/api/capture` | `{ text, to: "card" \| "diary", space? }`, with a session or `Authorization: Bearer` a token |
+| `GET`/`POST` | `/api/tokens` | the capture tokens / make one, `{ name }`, answered with the token this once |
+| `DELETE` | `/api/tokens/{id}` | remove a capture token |
+| `POST` | `/api/spaces/{id}/lists` | add a list |
+| `PUT`/`DELETE` | `/api/lists/{id}` | rename / delete a list and its cards |
+| `POST` | `/api/lists/{id}/cards` | add a card |
+| `PUT`/`DELETE` | `/api/cards/{id}` | update (title, body, due, repeat, done, link) / delete; finishing a repeating card answers its copy as `again` |
+| `POST` | `/api/cards/{id}/move` | to another list and place in it; `again` as above |
+| `POST` | `/api/nodes/{id}/move` | reparent, and place among the new siblings |
+| `POST` | `/api/nodes/move` | `{ ids, parent_id }`: several into one folder, all or none |
+| `POST` | `/api/nodes/{id}/import` | import a folder or zip (multipart) under this node |
+| `POST` | `/api/import` | import an export at the root: directories become spaces |
+| `GET` | `/api/resolve?path=` | the node behind a slug path, `work/notes/monday` |
+| `GET` | `/api/nodes/{id}/revisions` | a document's earlier versions, the newest first |
+| `GET` | `/api/nodes/{id}/revisions/{rev}` | one earlier version, with its text |
+| `GET` | `/api/nodes/{id}/backlinks` | the documents that link to this one, with the line each does it on |
+| `POST`/`DELETE` | `/api/nodes/{id}/share` | mint or rotate / revoke a share link |
+| `GET` | `/api/export` | the whole workspace, boards and dates included, as a zip |
+| `GET`/`POST` | `/api/backup` | Proton Drive mirror: status / run now |
+| `POST`/`DELETE` | `/api/backup/login` | log the mirror in to Proton, body `{"username","password"}` / forget its session |
+| `POST` | `/api/backup/login/totp`, `/api/backup/login/captcha` | the two-factor code `{"code"}`, or the solved human check `{"token"}`, a waiting login asked for |
+| `GET`/`POST` | `/api/media` | list / upload (multipart) |
+| `GET`/`DELETE` | `/api/media/{id}` | serve (with `Range`) / delete a file nothing in the tree owns |
+| `GET` | `/api/spaces/overview` | every space with what was last written in it |
+| `POST` | `/api/nodes/{id}/files` | upload files into this folder as file nodes (`?encrypt=true` seals them) |
+| `POST` | `/api/nodes/{id}/inspect` | what a certificate, key or keystore file holds (`{password}` opens a protected one) |
+| `POST` | `/api/nodes/{id}/encrypt`, `/decrypt` | seal a file, or a folder for good — what is in it and what lands later — or take it out |
+| `GET`/`POST`/`PUT` | `/api/vault` | status / set the password once / change it |
+| `POST` | `/api/vault/unlock`, `/api/vault/lock` | open / close the vault |
+| `GET`/`POST` | `/api/ssh-keys` | the keys that may mount over SFTP / add one |
+| `DELETE` | `/api/ssh-keys/{id}` | remove a key, which ends its mounts |
+| `POST` | `/api/share/{token}` | public: read a shared document, body `{"key"}`; sets the share's media cookie |
+| `GET` | `/api/share/{token}/media/{id}` | public: media inside a shared document, with that cookie |
diff --git a/docs/wiki/Backup-and-Mail.md b/docs/wiki/Backup-and-Mail.md
new file mode 100644
index 0000000..f072f38
--- /dev/null
+++ b/docs/wiki/Backup-and-Mail.md
@@ -0,0 +1,97 @@
+# Backup and mail
+
+## Backing up to Proton Drive
+
+The server can mirror its data directory into Proton Drive, where it appears as
+a device of its own, beside the desktop clients. Everything is encrypted before
+it leaves the machine, by the same Rust SDK the Linux client uses, so Proton
+stores a workspace it cannot read.
+
+It is a third-party client. It names itself to Proton as
+`external-drive-narl_workspace@-alpha`, carries no Proton branding,
+and is not supported by Proton.
+
+### Logging in
+
+Once, from the workspace:
+
+```text
+:backup login
+```
+
+It shows the disclosure above and asks before anything else, then asks for the
+password and, when the account has one, the two-factor code. If Proton wants a
+human check first, its own page opens in a sheet. An account whose only second
+factor is a security key cannot log in from here; add an authenticator app to
+it. The mirror starts within a minute; no restart is needed.
+
+The same login runs from the server's terminal:
+
+```sh
+narl-workspace proton-login
+docker compose exec -it workspace narl-workspace proton-login # in Docker
+```
+
+The password itself is never stored, only the session and the key passphrases
+derived at login. They are kept as a `0600` file next to the database, or in
+the OS keyring with `--features keyring`. In a container,
+`WORKSPACE_SESSION_KEY_FILE` seals that file under a key kept off the data
+volume, such as a Docker secret.
+
+### What it does
+
+The mirror is one-way and runs after changes: once the workspace has been
+quiet for `WORKSPACE_BACKUP_DEBOUNCE_SEC`, with `WORKSPACE_BACKUP_INTERVAL_MIN`
+as a backstop. An hour of writing is one backup, not sixty.
+
+```text
+narl-workspace/ the device
+ data/ the data directory, copied
+ RESTORE.txt what this is, and how to put it back
+ diary.db a consistent snapshot
+ uploads/ every uploaded file
+ files/ every file in the tree again, as //
+```
+
+- `diary.db` becomes a new revision on Drive each time, so Proton keeps the
+ older ones and a mistake that was mirrored can still be undone.
+- `files/` is for reading on Drive by name. It follows renames and moves.
+- Restoring is copying the contents of `data/` into an empty data directory.
+- A delete here does not delete on Drive unless `WORKSPACE_BACKUP_PRUNE` is on.
+
+| command | what it does |
+| --- | --- |
+| `:backup` | when the last one finished, and whether it failed |
+| `:backup!` | run one now |
+| `:backup logout` | forget the session; the mirror stays where it is |
+| `narl-workspace proton-status` | account, device, schedule, how much is mirrored |
+| `narl-workspace backup-now` | mirror once and exit, for cron |
+
+## Mail
+
+Mail is off until `WORKSPACE_SMTP_URL` is set. Then four kinds of plain-text
+mail go to `WORKSPACE_MAIL_TO`:
+
+| kind | when |
+| --- | --- |
+| `reminder` | at `WORKSPACE_REMINDER_AT`, when nothing was written in the diary that day |
+| `card_due` | at `WORKSPACE_CARD_DUE_AT`, for every card due that day, and again once it is overdue |
+| `digest` | at `WORKSPACE_DIGEST_AT`: what was written in each space, what was finished, what is still open |
+| `login` | a browser that has not signed in before signed in, or a run of wrong passwords |
+
+`WORKSPACE_MAIL_KINDS` lists the kinds that may be sent. Every "today" is
+counted in `WORKSPACE_TIMEZONE`.
+
+Mail goes through an outbox: a restart cannot send yesterday's reminder twice,
+and a relay that refuses cannot lose one. It is retried with a widening gap, up
+to an hour apart, and given up after ten refusals. A card mail is checked
+against its card before it goes, so a card finished since is not mailed.
+
+```sh
+narl-workspace mail-test # one mail, straight out
+narl-workspace mail-status # what is configured, queued and failed
+```
+
+`compose.override.yml.example` has a [mailpit](https://mailpit.axllent.org/)
+service for trying the settings out: point `WORKSPACE_SMTP_URL` at
+`smtp://mailpit:1025` and read the mail at .
diff --git a/docs/wiki/Boards-and-Agenda.md b/docs/wiki/Boards-and-Agenda.md
new file mode 100644
index 0000000..20c979e
--- /dev/null
+++ b/docs/wiki/Boards-and-Agenda.md
@@ -0,0 +1,113 @@
+# Boards and the agenda
+
+## The board
+
+Every space except the diary has a board. `b`, the board button on the rail, or
+`:board` swaps the tree for it, and `q` swaps back.
+
+
+
+A new board has three lists: `backlog`, `doing` and `done`. The lists mean
+something by where they stand, not by their names:
+
+- the **first** list is the inbox, where new cards land;
+- the **last** list is done. A card is finished exactly when it is in the last
+ list: moving it there stamps it, moving it out reopens it;
+- the lists between are **work in hand**, which the agenda shows.
+
+`:list ` adds a list in front of the last one, `:name!` renames the list
+under the cursor, and `:list!` deletes it with its cards. Done cards older than
+a week fold away until `z` shows them.
+
+| key | what it does |
+| --- | --- |
+| `h` / `l` | to the list on the left / right |
+| `j` / `k` | down / up the cards |
+| `J` / `K` | move the card down / up its list |
+| `H` / `L` | move the card to the list on the left / right |
+| `Space` | into the done list, or back out to the inbox |
+| `o`, `c` | a new card on this list |
+| `Enter` | the card's notes |
+| `D` | due date |
+| `R` | rename the card |
+| `z` | show done cards older than a week |
+| `F` | a letter over every card; type it to jump there |
+| `x`, `dd` | delete the card |
+| `q`, `Esc` | back to the tree |
+
+### Capturing a card
+
+`c` anywhere outside the board opens `:card ` on the command line. The card goes
+into the inbox of the space you are in, or of the first space with a board, and
+you stay where you were. From outside the browser, see [Capture](Capture.md).
+
+### Notes
+
+`Enter` on a card opens its notes: a document named after the card, made in the
+root of the space the first time. Closing it goes back to the board, on the
+same card. `:card!` while reading a document makes a card that points at it.
+
+The link is loose on purpose: deleting the document leaves the card, because the
+work is still open when its notes are gone.
+
+### Due dates
+
+`D` on a card (or `:due `) takes a date the way it is said:
+
+| you type | it means |
+| --- | --- |
+| `today`, `td` | today |
+| `tm`, `tomorrow` | tomorrow |
+| `fri` | the next Friday, never today |
+| `+3d`, `+2w` | in three days, in two weeks |
+| `10-31` | the next 31 October |
+| `2026-10-31` | that day |
+| `-` | no due date |
+
+### Cards that come back
+
+`:repeat weekly`, or the ↻ button on the rail, makes a card come back once it
+is done. The rules are `daily`, `weekly`, `monthly`, `yearly`, and any `+3d`,
+`+2w`, `+6m` or `+1y`. `:repeat -` stops it.
+
+Finishing such a card leaves a copy at the bottom of the inbox, with the same
+title, notes and rule, due on the next date. The next date counts on from the
+old due date, in `WORKSPACE_TIMEZONE`:
+
+- a card due at noon stays due at noon across a change of the clocks;
+- a card due on the 31st comes back on the last day of a shorter month, and on
+ the 31st again where there is one;
+- a card finished late skips the dates that have passed;
+- a card without a due date counts from the moment it was finished.
+
+The finished card gives up its rule, so reopening it and finishing it again
+does not make a second copy. The board and the agenda show the rule as
+`↻ weekly` beside the date.
+
+## The agenda
+
+`t` (or `:agenda`) gathers what needs you from every board at once.
+
+
+
+- **late**, **today** and **this week**: unfinished cards by due date;
+- **in hand**: cards in a list between the inbox and done;
+- whether the diary has an entry for today;
+- **on this day**: diary entries from the same day in earlier years. A
+ 29 February is remembered on the 28th in the years that have none.
+
+| key | what it does |
+| --- | --- |
+| `j` / `k` | down / up, on into the diary entries |
+| `Enter` | to the card on its own board, or open the entry |
+| `Space` | finish the card where it stands |
+| `D` | due date |
+| `H` / `L` | to the list left / right on its board |
+| `:` | the command line with the agenda kept up, so `:repeat` acts on the card |
+| `T` | today's diary entry |
+| `t`, `q`, `Esc` | close |
+
+The agenda opens by itself once a day when something is late or due. The bar
+shows the `late` and `today` counts whenever either is above zero. With mail
+set up, cards due today are mailed in the morning too; see
+[Backup and mail](Backup-and-Mail.md#mail).
diff --git a/docs/wiki/Capture.md b/docs/wiki/Capture.md
new file mode 100644
index 0000000..87995ee
--- /dev/null
+++ b/docs/wiki/Capture.md
@@ -0,0 +1,56 @@
+# Capture
+
+A thought arrives when no tab is open. `POST /api/capture` writes it down from
+anything that can send a request: a phone shortcut, a script, `curl`. It takes a
+token in place of a login.
+
+## Tokens
+
+```text
+:token add phone makes one, shows it once and copies it
+:token lists them, with when each was last used
+:token rm phone and that one captures nothing from the next request on
+```
+
+A token opens this one route and nothing else. It cannot read anything, not even
+the list of spaces. Only its SHA-256 is kept on the server, so it is shown once,
+when it is made. Wrong tokens slow down the way wrong passwords do, on a line of
+their own, so guessing at them does not hold up the login.
+
+## Sending
+
+```sh
+curl -H "Authorization: Bearer nwc_…" -H 'content-type: application/json' \
+ -d '{"text":"call the bank"}' https://your-host/api/capture
+```
+
+| field | meaning |
+| --- | --- |
+| `text` | what to write down; for a card, the first line is the title and the rest its notes |
+| `to` | `"card"` (the default) or `"diary"` |
+| `space` | for a card, the slug of the space whose inbox it goes in |
+
+- `"to": "card"` puts a card in the inbox of the space named by `space`, or else
+ of the first space that has a board.
+- `"to": "diary"` adds `- 14:05 call the bank` to the end of today's entry, in
+ `WORKSPACE_TIMEZONE`, and starts the entry from `templates/day` when there is
+ none yet.
+
+The answer says where it went: `{"to":"card","id":7,"where":"work/backlog"}`.
+
+## From a phone
+
+**iPhone**: in Shortcuts, make a shortcut with *Ask for Input*, then *Get
+Contents of URL*:
+
+- URL: `https://your-host/api/capture`
+- Method: `POST`
+- Headers: `Authorization` = `Bearer nwc_…`
+- Request body: JSON, with `text` set to the *Provided Input*, and `to` set to
+ `diary` if that is where it should go.
+
+**Android**: in [HTTP Shortcuts](https://http-shortcuts.rmy.ch/), make a shortcut
+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.
diff --git a/docs/wiki/Commands.md b/docs/wiki/Commands.md
new file mode 100644
index 0000000..75bb9ce
--- /dev/null
+++ b/docs/wiki/Commands.md
@@ -0,0 +1,222 @@
+
+
+# Keys and commands
+
+Everything here is also on `?` (`:help`) inside the app, which draws the same
+lists. Keys act on whatever has focus: the row under the cursor in the list, or
+the document in the focused tile. Inside the editor the full vim keymap is live,
+and `:` opens the command line, where `Tab` completes commands and their
+arguments and `↑` / `↓` recall earlier lines.
+
+## Keys
+
+### Browsing and reading
+
+| key | what it does |
+| --- | --- |
+| `1 … 9 · 0` | switch to that space, like a workspace |
+| `Shift 1 … 9` | send the marked nodes, or the selected one, to that space |
+| `Tab · Shift-Tab` | next / previous space |
+| `w` | overview of every space — hjkl, then Enter |
+| `f` | fullscreen the focused pane or tile; again or Esc to leave |
+| `Alt-h/j/k/l` | focus the tile that way, from the editor too — left of the leftmost is the list |
+| `Alt-Shift-h/j/k/l` | move the focused tile that way |
+| `L · Shift-Enter` | open the row in a new tile beside the others |
+| `gt / gT` | next / previous tile — in the editor too |
+| `Alt-q` | close the focused tile — :close; :only closes the others |
+| `j / k · ↓ ↑` | move down / up the list — scroll the document when it has focus |
+| `gg / G` | first / last row, or top / bottom of the document — a first g, d, ] or [ waits a second in the status line |
+| `Ctrl-D / Ctrl-U` | half a page down / up the open document |
+| `zl / zh · zL / zH` | the wide table or code block at the top of the view, sideways — a little, or half its width |
+| `]] / \[\[` | next / previous heading of the document — in the editor too |
+| `gO` | the headings as a list: j/k, Enter jumps there |
+| `F` | hints over the links and buttons in view — type one to follow it |
+| `gf / gx` | in the editor: follow the link under the cursor / open it in a tab |
+| `gx` | open the file under the cursor in a tab of its own |
+| `Enter · l` | open a document, or enter a folder — on an open keystore or sealed file, Enter asks for its password |
+| `h` | from the document to the list, from the list up a level |
+| `o` | new document here, straight into insert mode |
+| `O` | new folder here |
+| `i · a` | edit the open document |
+| `R` | rename what is under the cursor — document, folder or card — starting from its name |
+| `m / p` | pick nodes up, then drop them all into the folder you walk to |
+| `M` | pick up the whole folder — again puts it all back |
+| `Ctrl / Shift-click` | pick a row up · every row up to it — drag them onto a folder |
+| `t` | agenda — late, due this week, in hand, on every board |
+| `T` | today's diary entry, started if there is none — from templates/day when the diary has one |
+| `C` | the diary by month: h/l a day, j/k a week, H/L a month, . today, Enter opens or starts the day |
+| `[d / ]d` | the diary entry before / after the one open, by date |
+| `Ctrl-P` | find anything, in every space — Enter goes to the result lit |
+| `b` | open this space's board |
+| `c` | capture a card into this space's inbox, without leaving here |
+| `Esc · q` | close the document, and its tile when there are several |
+| `/` | full-text search, inside the current space and its text files — Tab or Ctrl-N/P walk the results, Enter opens one |
+| `n / N` | next / previous search result, opened beside the results — :noh clears the search |
+| `s` | toggle sharing for the document — asks before taking it back |
+| `y` | mint a new share link and copy it — asks, the old one stops working |
+| `x · dd` | delete what has focus — the row in the list, the document in its pane — into the trash (asks y/N) |
+| `u` | undo the last delete: it comes back out of the trash — :trash has the rest |
+| `gh` | the open document's earlier versions, as diffs — Enter brings one back |
+| `gb` | the documents that link to the open one — \[\[title]] links by title, \[\[ completes one while writing |
+| `:` | command line — Tab / Shift-Tab cycle the matches, then the argument; ↑ ↓ recall what ran before |
+| `Ctrl-S` | save, from anywhere |
+| `?` | this screen — in the editor too |
+
+### On a board
+
+The first list is the inbox, the last list is done.
+
+| key | what it does |
+| --- | --- |
+| `h / l` | to the list left / right |
+| `j / k` | down / up the cards |
+| `gg / G` | first / last card of the list |
+| `J / K` | move the card down / up its list |
+| `H / L` | move the card to the list left / right |
+| `Space` | into the done list, or back out to the inbox |
+| `Enter` | the card's notes — a document, made on first use; q comes back |
+| `o · c` | new card on this list |
+| `D` | due date: fri, tm, +3d, 10-31 … |
+| `R` | rename the card, or the list when it is empty |
+| `z` | show done cards older than a week |
+| `F` | a letter over every card in view — type it to put the cursor there |
+| `r` | read the board again, for changes made elsewhere |
+| `x · dd` | delete the card (asks y/N) |
+| `q · Esc` | back to the tree |
+
+### In the agenda
+
+| key | what it does |
+| --- | --- |
+| `j / k` | down / up the cards, then what was written on this day in earlier years |
+| `Enter` | to the card, on its own board — or to the entry |
+| `Space` | done, where it stands |
+| `D` | due date: fri, tm, +3d … — :due on the line |
+| `:` | the line, with the agenda kept up — :repeat weekly acts on the card |
+| `H / L` | to the list left / right on its board |
+| `T` | today's diary entry |
+| `t · q · Esc` | close the agenda |
+
+## Commands
+
+A command takes a `!` for its forcing variant where one exists, the way vim's
+do: `:w!`, `:space! `, `:import!`.
+
+### Spaces & folders
+
+| command | what it does |
+| --- | --- |
+| `:space, :sp [name]` | switch to a space, or list them |
+| `:space!` | create a space with that name and switch to it |
+| `:mkdir ` | make a folder here |
+| `:cd ` | enter a folder, .. goes up, / back to the space |
+| `:import, :imp` | upload a folder into this one — markdown becomes documents, links follow |
+| `:import!` | import loose files, or a .zip of a folder; :import! / pours an :export! back in, spaces and boards and all |
+| `:move, :mv ` | move the marked nodes, or else the selected one, into a folder here, up one level, or to the root of the space |
+| `:unmark` | put down everything picked up with m |
+| `:rmspace, :rms [name]` | delete a space and everything in it, into the trash (asks first) |
+| `:trash [purge]` | what was deleted — j/k, Enter brings it back, x deletes it for good; :trash purge empties it |
+
+### Boards
+
+| command | what it does |
+| --- | --- |
+| `:board` | show this space's board, or go back to the tree |
+| `:card, :c ` | add a card — on the board to the list under the cursor, elsewhere to this space's inbox |
+| `:card!` | add a card linked to the open document — Enter on it opens the document |
+| `:due ` | due date of the card — on the board, or highlighted in the agenda: today, tm, fri, +3d, +2w, 10-31, 2026-10-31 — - clears |
+| `:repeat ` | once the card is done, a copy comes back in the inbox: daily, weekly, monthly, yearly, +3d, +2w, +6m — - stops it |
+| `:done` | move the selected card into the done list, or back out to the inbox |
+| `:list ` | add a column to the board |
+| `:list!` | delete the column under the cursor and every card on it |
+| `:agenda, :ag` | what is late, due this week, or in hand — across every board |
+
+### Documents
+
+| command | what it does |
+| --- | --- |
+| `:new, :n, :o [template] [yyyy-mm-dd] [name]` | start a new document here — from one of the space's templates, dated, named; in the diary from templates/day |
+| `:cal, :calendar` | the diary by month — h/l a day, j/k a week, H/L a month, Enter opens the day or starts it |
+| `:today` | open today's diary entry, starting one if there is none yet |
+| `:next` | open the next node down the list |
+| `:prev` | open the previous node |
+| `:first` | open the first node in the list |
+| `:last` | open the last node in the list |
+| `:random` | open something here at random — good for re-reading |
+| `:delete, :d, :rm [id]` | delete a node, and everything under it, into the trash (asks first) — u brings it back |
+| `:history, :hist` | the open document's earlier versions as diffs — j/k, Enter brings one back (gh) |
+| `:backlinks, :bl` | the documents that link to the open one — j/k, Enter opens one (gb) |
+| `:source` | show an open markdown file as written, or rendered again |
+| `:outline, :toc` | the open document's headings to pick from — gO, in the editor too |
+| `:search, :se [text]` | full-text search in this space (empty clears) |
+| `:search!` | full-text search across every space — Ctrl-P |
+| `:clear, :noh` | clear the search filter |
+| `:reload, :r` | re-read the current folder from the server |
+
+### Tiles
+
+| command | what it does |
+| --- | --- |
+| `:close, :clo` | close the focused tile — Alt-q; asks when its draft could not be written |
+| `:only, :on` | close every tile but the focused one |
+| `:ls, :buffers, :files` | list the open tiles, % on the focused one |
+| `:buffer, :b ` | focus a tile by its number in :ls, or by the start of its name |
+| `:bnext, :bn` | the next tile — gt |
+| `:bprevious, :bp, :bN` | the previous tile — gT |
+
+### Writing
+
+| command | what it does |
+| --- | --- |
+| `:write, :w` | save the open document |
+| `:write!` | save even over a newer version saved somewhere else since this one was opened |
+| `:wq, :x` | save and leave insert/editor mode |
+| `:wq!` | save over a newer version, as :w! does, and leave the editor |
+| `:quit, :q` | close the editor, or the document when already reading |
+| `:quit!` | discard changes and close the editor |
+| `:edit, :e [id]` | edit the open document, or open a node by id |
+| `:edit!` | throw away the draft and re-read the document from the server |
+| `:name, :title, :rename ` | rename what is under the cursor — document, folder, or card on a board; bare, while writing, into the title |
+| `:name!` | rename the space you are in — on the board, the list under the cursor |
+| `:date ` | re-date the open document |
+| `:upload, :up` | while writing, attach files at the cursor — otherwise put files into this folder |
+| `:upload!` | put files into this folder sealed in the vault — encrypted before they touch the disk |
+| `:media` | browse everything you have uploaded — j/k, i inserts it while writing, y copies its address, x deletes |
+
+### Sharing & export
+
+| command | what it does |
+| --- | --- |
+| `:share` | publish the document behind an unguessable link and copy it |
+| `:unshare` | revoke the share link |
+| `:link` | mint a new share link for the document and copy it |
+| `:copy, :yank` | copy the whole document to the clipboard as markdown |
+| `:export, :exp` | download the open document as a .md file |
+| `:export!` | download the whole workspace — every space, document and file — as a zip |
+| `:download, :dl` | download the file under the cursor, or the open one — the open document goes as :export writes it |
+
+### Vault
+
+| command | what it does |
+| --- | --- |
+| `:vault` | whether the vault is open — or, the first time, set its password |
+| `:unlock` | open the vault with its password, so sealed files can be read |
+| `:lock` | close the vault now — it also closes itself when left alone, and on logout |
+| `:passwd` | change the vault's password — every sealed file is then re-sealed under a new key, while the vault is open |
+| `:encrypt, :seal` | seal the file under the cursor in the vault — a folder stays sealed: every file in it, and every file added later |
+| `:decrypt, :unseal` | take the file under the cursor back out of the vault — on a sealed folder, unseal it and every file inside |
+
+### Session
+
+| command | what it does |
+| --- | --- |
+| `:stats` | word, line and node counts |
+| `:set