From 480eab0c6a192207413ceb07db72d99decb31d46 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Wed, 21 Oct 2026 12:07:32 +0200 Subject: [PATCH] plan: what writing the docs turned up The feature plan has landed. PLAN.md now holds what the wiki, the screenshots and a round of probing the API turned up: a rename that writes over a newer save without asking, an update that empties a document when `body` is left out, a search of punctuation that lists the spaces, an agenda that keeps the browser's day rather than the workspace's, and rough edges from duplicate space names to a clone that does not compile before the frontend is built. Each says where, and how it could be fixed. The unplanned features from the last plan are carried over at the end. --- PLAN.md | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..4c41ee6 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,158 @@ +# Findings, possible bugs, and what comes next + +Written on 2026-10-21, after the seven slices of the feature plan had landed +(trash `8ffad63`, revisions `fc2be66`, links `a42f907`, capture tokens +`f629f5a`, diary `824f2bb`, recurring cards `6673298`, search in files +`909abf5`). That plan has fully landed, and git keeps it. + +These came out of writing the wiki, taking the screenshots against a fresh +workspace, and probing the API on a throwaway server. Each entry says what +happens, where, and a way to fix it. None of them loses data outright. Several +can overwrite text, but a revision of it is kept. + +## Bugs + +### 1. A rename can overwrite a newer save without asking + +`renameNode` (`web/src/lib/store.svelte.ts:1171`) renames by sending the whole +node: name, body and date, and no `base_updated_at`. That has two effects: + +- For the document open in a tile, the body sent is the local draft. If the + document was saved somewhere else since it was opened (another tab, the + phone, the SFTP mount), the rename writes the draft over that save. `:w` + would have refused and offered `:e!`; `:name` does not. +- For any other node, the body is fetched with `api.getNode` and sent back. A + save that lands between the two requests is overwritten. + +The server keeps a `force` revision in both cases (`nodes.rs:757`), so the +other text can be brought back with `gh`. Nothing says that it is needed. + +**Fix:** send `base_updated_at` from the tile, or from the node just fetched, +and handle the 409 the way `save` does. Better still, see 2. + +### 2. `PUT /api/nodes/{id}` without `body` empties the document + +`UpdateInput.body` defaults to `""` (`src/routes/nodes.rs:161`). A script, or a +future client, that renames a document with `{"name": "…"}` alone erases its +text. A `force` revision is kept, so it can be restored, but the API invites +the mistake. + +**Fix:** make `body` an `Option` and leave the text alone when it is +absent. The same for `created_at`, which already is optional. Then +`renameNode` can send only the name, and 1 goes away for every node that is not +being edited. + +### 3. A search made only of punctuation lists the spaces + +`fts_query` (`nodes.rs:251`) turns `!!!` or `--` into no terms. `list` then +falls through to browsing with `parent = None` (`nodes.rs:514`), and answers +with the list of spaces, as if they were search results: on a throwaway +server, `GET /api/nodes?q=%21%21%21` answered with every space. + +**Fix:** when `q` is present but yields no terms, answer an empty list. + +### 4. The agenda opens once per browser day, not per workspace day + +`openAgendaIfDue` (`store.svelte.ts:1311`) keys "seen today" on +`new Date().toDateString()`, the browser's local day. The server counts days in +`WORKSPACE_TIMEZONE`. A browser in another timezone than the setting (a laptop +on a trip, a browser set to UTC) opens the agenda at the wrong moment, or twice +on one workspace day. + +**Fix:** key it on the server's local day, which the agenda answer could carry +next to `diary_today`. + +## Rough edges + +### 5. Two spaces can have the same name + +`create_space` (`nodes.rs:458`) makes the slug unique (`work-2`) but keeps the +name, so the bar shows two pills that read alike, and `:space work` picks the +first. Folders and documents may share names on purpose; spaces are a short, +numbered list where that is only confusing. + +**Fix:** refuse a space whose name is taken, case-insensitively, with the +reason. + +### 6. Names have no length limit + +A 5000-character name is accepted, and so is its slug, which ends up in every +`/n/…` address and in the SFTP mount, where most filesystems refuse names over +255 bytes. + +**Fix:** cap names (255 characters, say) on the server, and cut slugs shorter +than that. + +### 7. A hard-wrapped paragraph renders with its line breaks + +`marked` runs with `breaks: true` (`web/src/lib/markdown.ts:6`), so a single +newline is a `
`. That suits text typed in the browser, but markdown that +comes from elsewhere is often wrapped at 80 columns: an import, a file saved +over the SFTP mount from vim with `textwidth`, a pasted README. Those read with +a break in the middle of every sentence, more visibly on a phone. + +**Fix:** a `:set nobreaks` per document or per space, or treating a line break +as a space when the paragraph's lines are all close to the same width. At the +least, say so in the docs; the wiki's Writing page now does. + +### 8. A fresh clone does not compile until the frontend is built + +`rust-embed` needs `web/dist` to exist (`src/static_files.rs:10`), so +`cargo check`, `cargo test` and rust-analyzer fail on a fresh clone with a +derive error that does not say what to do. CI builds the frontend first. + +**Fix:** `#[allow_missing = true]` on the embed, so a missing frontend is the +404 the handler already answers with ("frontend not built"), not a compile +error. + +### 9. Search inside files: known limits + +- `file_text::catch_up` scans every file node without a row on each upload, + import and SFTP write. That is fast at a few thousand files and will not + stay so. +- Results from documents and from files are ranked by two separate BM25 + indexes, so their order relative to each other is approximate. +- The excerpt does not mark the words that matched. +- PDFs and office documents are not read. + +**Fix:** refresh only the node that changed, and keep the full scan for start +up. Mark the match in the snippet with a character the client draws as +highlight. + +## Docs and tooling, done in this pass + +- An icon, `~/` and a block cursor, as the favicon, the touch icon, and the + mark in the bar, the login and the share page. +- A README that opens with the banner, the badges, screenshots and the + features grouped by what they are for. +- A wiki in `docs/wiki`: a getting-started page, a guide that walks through a + day, a page per part of the app, and three reference pages generated from the + code (`scripts/wiki.ts`). +- Screenshots in `docs/images`, taken by `scripts/screenshots.ts` against a + throwaway workspace, so they can be taken again when the UI changes. +- CI: `.github/workflows/ci.yml` runs the Rust checks, svelte-check and the + wiki check on every push. `.github/workflows/wiki.yml` publishes the wiki on + every push to `main`. + +**Before the first wiki publish:** GitHub creates a wiki's repository only +when its first page is saved, so save any page once in the repository's Wiki +tab. The workflow replaces it on its first run. + +## Next, unplanned + +Carried over from the last plan, still not started: + +- 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. The new icon is ready for the manifest. +- Task lists: toggle `- [ ]` in the reader, and `:tasks` gathers the open ones + in a space. +- A daily review in the agenda: yesterday's entry, what was finished this week, + and cards in `doing` for more than 14 days. +- Marks: `ma` … `mz` per document, `'a` to jump. This clashes with `m` for + picking nodes up, so choose the keys first. +- Mail-in capture: IMAP polling of a plus-address into the inbox. + +Suggested order: 2 and 1 together (one change, and the only findings that can +overwrite text), then 3, 8 and 5, which are small. Then the PWA, which the new +icon makes cheap.