# Changelog
All notable changes to this project are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
This changelog is written in English only.
## [Unreleased]
## [4.3.0] - 2026-10-01
### Upgrade notes
- No configuration or API changes. Static asset cache keys changed, so browsers fetch the new
CSS/JS on the first load after the upgrade.
- The Neural Atlas now plays a short showcase on every visit, also with the OS setting
"reduce motion" (2.5 s instead of 7 s), then rests; see `docs/ui-accessibility.md`.
### Added
- **Recall — one place to find anything.** Press ⌘K / Ctrl+K anywhere, or
/ outside text fields, or tap the magnifier in the header on mobile. Recall searches
notes as you type with prefix matching ("oau" finds "OAuth", which the header search does not),
shows recently opened notes when empty and offers the main actions (new note, Neural Atlas,
tags, search history, sidebar, language, settings for admins) in the same list.
- Keyboard first: ↑/↓ (or Tab, Ctrl+N/P) select, ↵ opens, Esc closes and returns focus to where
it was. Pressing ↵ before results arrive waits for the fresh results instead of opening a
stale entry. Superseded requests are aborted.
- Accessible as a combobox with `aria-activedescendant`; all copy in German and English.
- Recently opened notes are kept per browser in `localStorage` (path and title only).
- **Reading progress** as a thin amber line under the header while a note is open.
- **Neural Atlas: synapse field and data flow like the kiwiki.xyz hero.** A decorative field of
72 points (40 below 760 px) surrounds the user's graph, joins the build-up, rotates with the
camera and carries its own data packets. It is never labelled or hit-tested, dims while a node
is selected and is skipped for graphs above 180 nodes, which fill the space themselves.
- Data packets on graph and field edges are drawn with a glowing trail and a bright core; a new
packet starts every 220–440 ms and hops on at the target with 60 % probability (caps: 32 on
the graph, 36 on the field).
- Every 4–7 s a visible front document is **recalled**: flash, ring, bold label for 2.6 s,
highlighted edges and hot packets to all neighbours (up to 8).
- **Neural Atlas plays briefly, then rests.** Every visit shows the build-up plus about 7 s of data
flow (2.5 s with `prefers-reduced-motion`); then packets and rotation fade out over 1.8 s and the
atlas stands still with a ▶ button. ▶ keeps it moving until ⏸, which fades out again in 0.7 s.
The choice is not stored; every visit gets the short showcase.
- **App motion:** the desktop sidebar slides open and closed (width plus staggered content, tree
rows run in on open), the mobile sidebar slides in with its content; folders unfold their
children in sequence; the account menu grows out of its button; the home page and the atlas
sidebar/HUD build up in sequence. All of it is off with `prefers-reduced-motion`.
### Changed
- **UI review fixes (measured, then corrected):**
- The sidebar tree filter is gone (search lives in the header and Recall); the **Knowledge graph
entry moved to the top of the sidebar** and out of the account menu.
- **Header search is centred in the window** (was 84 px left of centre) via a three-column
header; the magnifier sits inside its button with 15 px to the placeholder.
- **Login card is vertically centred** — the shared `body { min-height: 100% }` had overridden
the login page's `100dvh`, leaving 16 px above and 311 px below the card.
- Neural Atlas **back link** uses a real 18 px icon in a 40 px target instead of a 12 px text arrow.
- Spacing pass: breadcrumbs, tag chips, role pills, keyboard hints and file actions get at least
0.6 em side padding and 2.2× line height; tree chevrons 10 → 14 px; select toggle 30 → 40 px.
- **Neural Atlas motion now matches kiwiki.xyz:** rotation ~5°/s, denser impulses, nodes near the
pointer light up, every node glows on small graphs, and the camera keeps re-fitting while the
layout settles (robust 95th-percentile radius) so the graph fills the stage — span at 1440×900
went from 396 × 276 px to 528 × 312 px.
- **Neural Atlas comes alive.** Same visual language as the new kiwiki.xyz hero:
- **Build-up on load:** nodes spiral in from the outside to their place (inner ones first),
glow briefly as they land, edges grow only once both ends have arrived, and a light wave
closes the sequence and fires the first impulses. Nodes still in flight cannot be selected.
Runs on every visit, including with `prefers-reduced-motion` (see "plays briefly, then
rests" below).
- **Action potentials:** impulses travel along edges, light up the node they reach and
sometimes jump on. Capped at 32 at a time; graphs above 180 nodes get no ambient impulses,
only the ones triggered by selection.
- **Selecting a node** sends a ring and an impulse down every connection (up to 14).
Hovering highlights the node's neighbours, their edges and labels.
- **Depth fog:** far nodes, edges and labels recede; near ones stay crisp.
- **Camera:** focus ("Explore connections") and reset glide instead of jumping; the view
re-fits once after the layout settles, so small graphs no longer sit in a fifth of the space
(minimum fitted distance 520 → 260). Manual wheel/pinch zoom disables the re-fit.
- **Idle rotation** 1.5 s after the last interaction; pauses while dragging and while the atlas
rests.
- **Material:** HUD, inspector and help bar with inner-light edge and soft depth, edge vignette
and static grain on the stage, softer inspector entrance.
- Glows are pre-rendered sprites (`drawImage`) instead of one radial gradient per node and frame;
the background gradient is cached per resize. Measured in headless Chromium: 59–61 fps with 21
nodes; with 500 nodes about 40 fps during the build-up and 44–60 fps after it (same range as
before the synapse field, which is not drawn for graphs that large).
- **Layered material.** Header, search results, Recall, toasts and dialogs are translucent with
blur; reading text and lists stay on solid surfaces. With `prefers-reduced-transparency` these
layers become opaque.
- **One motion language:** a spring curve for things that move into place (Recall, dialogs,
toasts, the selection lens, the segmented control) and a calm curve for state changes; notes fade
up when opened. With `prefers-reduced-motion` every state stays, only the travel is dropped.
- Segmented control on the home page ("Recently edited / created") gets a sliding thumb that is
measured to the active segment, so both labels keep the same 12 px side padding (equal-width
columns had left the shorter label with 16 px and the thumb 5 px short of the longer one); no gap
between the segments, and the bold label width is reserved so text does not jump on switch.
- Primary buttons, file header and panels get inner light and soft depth; the logo caret breathes
instead of blinking; the active note in the tree glows.
### Fixed
- Account button in the sidebar: the name turned amber ink on dark on hover (global
`button:hover` rule) and was unreadable.
- Neural Atlas pause button only ever showed the pause icon; it now switches to a play icon while
the atlas rests.
## [4.2.0] - 2026-09-30
### Upgrade notes
- **Theme customizations:** custom CSS that referenced `--md-warm` must switch to `--md-tertiary`.
### Changed
- **New brand color: honey amber (`#e3a94f`) replaces Kiwi Green.** Primary actions, focus rings,
links, the wordmark and the Neural Atlas now use amber so kiwiki is visually distinct from the
similar KiwiFS project. Tag nodes and the admin role pill moved to a cool blue (`--md-tertiary`,
formerly `--md-warm`) to stay distinguishable from the new primary. Static asset versions were
bumped so browsers pick up the new theme.
- **README screenshots** refreshed for the amber theme.
## [4.1.0] - 2026-09-29
### Upgrade notes
- **Refresh tokens are rotated.** `grant_type=refresh_token` now also returns a new
`refresh_token`; the redeemed one is consumed (`invalid_grant` on reuse). Spec-compliant OAuth
clients (ChatGPT, Claude) pick up the new token automatically.
- **Bearer headers no longer open UI routes.** Only `POST /` (JSON-RPC) accepts a bearer token
without a session; `/ui/*`, `/editor` and `/settings` require the browser session. REST (`/api/*`)
and MCP (`/mcp`) are unchanged.
- **Raw API-key cookies are deprecated.** They keep working but log a warning; please switch to
`Authorization: Bearer`.
### Security
- **A bearer header bypassed the session check on every UI route** — the WebAuthMiddleware let any
request carrying `Authorization: Bearer ` through to `/editor`, `/ui/*` and `/settings`
unchecked; data only stayed private because the namespace was missing. The bypass now applies to
`POST /` (JSON-RPC connectors) only, and `/ui/export` additionally requires an authenticated user.
- **Note content could set UI control classes** — `class` on `` was allowed freely (`kw-file-link`,
`btn btn-danger` …): app-styled buttons inside notes and global click handlers firing without data.
nh3 now filters classes through an allowlist (``: only `wikilink`/`missing`, ``: only
`language-*`), including the HTML export.
- **Session cookie without `Secure` on direct TLS** — the flag depended on `KIWIKI_TRUST_PROXY` only;
it now also follows the `https` request scheme.
- **Legacy cookie auth with a raw API key** logs a deprecation warning (once per user and process);
the limits of the in-memory refresh-replay protection are documented in `SECURITY.md`.
- **The OAuth registration counter grew without bound** — only the requesting IP was pruned. It is now
pruned globally and capped by `KIWIKI_OAUTH_MAX_REGISTER_SOURCES` (4096).
### Fixed
- **MCP `list_all_files` violated its outputSchema after a dashboard request** — the dashboard enriched
the cached entries with `excerpt`; strict clients rejected the whole response for up to 5 s.
`list_all_files` now returns copies.
- **Wikilinks in `~~~` blocks and inline code** turned into visible, escaped `` text; both now stay
literal. The duplicate-title check ignores leading horizontal rules and headings inside code blocks.
- **The editor preview did not resolve wikilinks relative to the source note** — it now matches the
server (verified by a test against `_resolve_wikilink`).
- **The editor preview was empty apart from wikilinks and code** — the custom text-node renderer
returned `undefined`, which ToastUI 3 treats as "render nothing". It now returns `context.origin()`;
the browser smoke test checks heading, paragraph, table cell and task text in the preview.
- **Note view without a visible main heading** — the server (`title_redundant`) and CSS
(`h1:first-child { display: none }`) together hid both titles. The CSS rule is gone; when the title
differs, body `h1` elements are demoted to `h2`, so exactly one `h1` remains.
- **Invisible selection checkboxes were tab stops** — every tree entry cost two tabs. Outside select
mode they now carry `tabindex="-1"`.
- **Escape collapsed the desktop sidebar** instead of closing open search results. Search results take
precedence; Escape now only closes the mobile overlay.
- **Excerpts showed raw wikilink syntax** (`[[../adr-001-sqliteADR 001]]`) and repeated the title.
Wikilinks become their label or file stem, and a leading heading equal to the title is dropped — in
the dashboard and in search results.
- **Task lists** (`- [ ]`/`- [x]`) render as disabled checkboxes in the note view, matching the editor
preview. The checkbox is inserted after nh3; note content still cannot inject an ``.
- **Knowledge page with the engine turned off** showed "0 nodes / 100 % depth" and a rebuild button that
could only fail. It now explains how to turn the engine on.
- **Directly opened note URLs showed an unstyled fragment page** — `/ui/file` returns an HTMX partial
without layout. Browser navigation (deep link, shared link, reload — detected by a missing
`HX-Request` header plus a `text/html` Accept) is now redirected with 307 to `/?file=`, which loads the
same note in full. Machine clients without `text/html` still get the fragment, so citation URLs remain
fetchable.
- **`[[Wikilinks]]` are clickable** — they used to be dead text in the note view and the editor preview.
Server-side (`_render_markdown_safe`) and in the ToastUI preview (`customHTMLRenderer`) they resolve
by Obsidian convention (relative to the folder, `.md` appended); missing targets carry the `missing`
class and a dashed underline, code blocks and inline code stay literal.
- **Frontmatter no longer leaks into the editor preview** — the `---` block, title and tags rendered as a
horizontal rule, heading and body text. Text nodes inside the frontmatter range are suppressed and empty
shells (rule, empty leading blocks) are removed by an observer; the source is untouched.
- **"Recently created" reported "no note" although notes existed** — the filter required a frontmatter
`created`. The fallback is now the filesystem time (birthtime, else ctime, else mtime); `updated`
without a stamp falls back to mtime.
- **Every timestamp read "Today, 00:00"** — writes stored only the date. `created`/`updated` now include
the time (UTC, minute precision); the display still converts to local time.
- **Search fields name their scope** — the top bar (`Suchen…`) and the sidebar (`Filtern…`) were two
magnifying glasses without a promise. Placeholders now carry verb and target ("Search notes…" /
"Filter file tree…", German equivalents).
- **Dashboard lists show a preview and tags** — previously only title and date, i.e. clicking blind.
Every row now has an excerpt (first ~120 characters without frontmatter/Markdown syntax, files over
100 KB excluded) and up to 3 tags.
- **The mobile CTA row fits at 375 px** — three buttons edge to edge are gone: only `+ New note` stays
full width, Tags/History move into a `···` menu (native ``, closes on selection). Section
labels use tighter tracking on mobile (0.18em → 0.06em).
- **The start page uses its empty half** — a new tag cloud (`/ui/tags?compact=1`, top 12 chips, no
breadcrumb or lists); without tags the section stays hidden.
- **Duplicate H1 in the note view** — when the frontmatter title equals the first body H1, the view
heading is hidden (breadcrumb and metadata remain); different titles are shown as before.
- **Internal `.kiwiki` files were readable through read APIs** — `validate_content_read_path()` existed
but was only called by `read_lines`/`file_info`. `read_file`, `fetch` and `read_many` (and with them
`/api/file`, `/ui/file` and the editor) still served `.kiwiki/agent_log.jsonl`. The guard now sits
centrally in `read_file()` and `_read_frontmatter_only()`; writing was already blocked and stays so.
- **MCP and UI search had no query limit** — only REST capped at 512 characters via `SearchRequest`.
`search()` now truncates server-side to `MAX_QUERY_LENGTH`, and the `search` tool schema declares
`maxLength: 512`. An unbounded megabyte query ran through FTS sanitising and the LIKE fallback and
burned CPU.
- **OAuth refresh tokens rotate** — previously a refresh token stayed reusable for 30 days. Every
successful redemption returns a new refresh token and consumes the presented one (reuse →
`invalid_grant`, in memory like codes and DCR clients). Failed client/resource binding checks
deliberately do not consume the token.
- **Open DCR registration is limited per IP** — 128 slots with a 24 h TTL could be filled from a single
source, after which legitimate clients got 503. Now at most 16 registrations per source IP and hour
(`KIWIKI_OAUTH_MAX_REGISTER_PER_IP`); only successful registrations count.
- **Unknown MCP methods return HTTP 200** — `-32601` was delivered as HTTP 404 and made the server look
broken. Method-not-found is a protocol error, not a transport error, and belongs in the JSON-RPC error
body with HTTP 200.
- **Stricter MCP schemas for OpenAI clients** — `fetch` requires either `id` (OpenAI contract) or `path`
(alias for direct callers) via `anyOf` instead of nothing; without `KIWIKI_BASE_URL` the startup logs a
warning, because citation URLs would be relative and unusable for OpenAI connectors.
- **Dev dependency `httpx2` raised to 2.13.1** — 6 known CVEs in 2.7.0 (WSS over SOCKS without TLS, SSE
ReDoS, multipart header injection, decompression bomb, request smuggling). Dev scope only; neither
`app/` nor `tests/` import the package, production was not reachable.
- **Live search never fired** — `hx-trigger` sat on the search form with the `changed` modifier. htmx
compares `elt.value` of the trigger element for that, and a `