# Architecture kiwiki is a FastAPI application with a server-rendered Jinja2 front end. There is no SPA framework by design — the UI stays cheap to operate on small devices and friendly to incremental improvement. This document describes how a request flows, where code lives, and the conventions helpers follow. Read this before changing anything in `app/templates/`, `app/static/`, or the UI endpoints in `app/main.py`. ## High-Level Layout ``` Browser ──HTTP──► FastAPI (app/main.py) │ ├── Middleware: request limits/IDs, rate limiter, security headers, CORS ├── Auth: cookie session OR Bearer API key (app/auth.py) ├── Tenancy: ContextVar sets per-user namespace (app/tenancy.py) │ ├── UI routes (/ui/*) → Jinja2 templates (app/templates/) ├── REST routes (/api/*) → JSON ├── MCP routes (/mcp, /mcp/sse, /oauth/*) (app/mcp_server.py) ├── MCP Git sandbox helpers (app/mcp_git.py) └── Static files (/static/*) (app/static/) ``` ## Tenancy Every request that touches storage runs inside a user namespace. `app/auth.py` calls `tenancy.set_user_ns(username)` after authenticating, which sets a `ContextVar` consumed by `app/storage.py` and `app/search.py`. This is why handlers can call `read_file(path)` without qualifying the path — the path is resolved against `/data//`. Two users can never read each other's files, search each other's index, or share MCP state. ## Request Flow Example: Note View 1. Browser issues GET `/ui/file?path=notes/demo.md` (ohne `HX-Request`-Header und mit `text/html`-Accept antwortet der Endpoint mit 307 auf `/?file=…` — HTMX-Partials haben kein Layout, Deep-Links schon) 2. `_session_user(request)` (in `app/main.py`) reads the `kiwiki_session` cookie and returns a `User` 3. `set_user_ns(user.username)` is applied via the auth dependency 4. `read_file("notes/demo.md")` resolves to `/data//notes/demo.md` via `tenancy.user_root()` 5. The Markdown passes `_linkify_wikilinks` (`[[Ziel]]` → interne Links, Obsidian-Konvention: relativ zum Ordner, `.md`-Ergänzung, fehlende Ziele als `.missing`), then sanitized through `nh3` and rendered to HTML via `markdown` 6. `templates.TemplateResponse` renders `partials/file_view.html` with the context: - `title`, `title_redundant` (True, wenn der Frontmatter-Titel der ersten Body-H1 gleicht — dann entfällt die View-H1, Breadcrumb + Meta bleiben), `path`, `updated`, `owner`, `tags` - `rendered` — sanitized HTML - `user` — the authenticated user (used by the template's role guard) - `can_delete` — write-or-admin flag for the delete button - `svg_edit`, `svg_trash` — pre-rendered icon strings 7. The partial is swapped into `#main-content` by HTMX (`loadFile()` in `kiwiki.js`) The `user` key is the historically lost piece that caused the v2.1.1 "edit button missing" bug — the guard `{% if user and user.role in ['write','admin'] %}` evaluated to `Undefined -> False`. Always include `user` in templates that perform role-based rendering. ## Template Hierarchy ``` layout.html Base: , header, sidebar slot,
, motion bundle ├── index.html Home dashboard (hero, step grid, recent, FAB, file tree) ├── editor.html /editor — Toast UI editor (own extra styles, own save flow) ├── settings.html /settings — admin-only user management └── login.html /login — standalone (does NOT extend layout.html) ``` `layout.html` defines the `{% block sidebar %}`, `{% block content %}`, and `{% block extra_styles %}` slots used by index/editor/settings. `login.html` keeps its own CSS because it loads before any session exists. ## Startseite (`index.html`) Layout-Reihenfolge im `{% block content %}`: 1. **Hero-Block** — kiwiki-Titel, Tagline („Dein persönlicher Wissensspeicher…"), „Neue Notiz"/„Tags"/„Suchverlauf" 2. **Dashboard** — Zwei Panels: „Zuletzt bearbeitet" + „Zuletzt erstellt" (je bis zu 8 Dateien, rekursiv via `list_all_files`) 3. **`
`** — eingeklappt: „Erste Schritte" (4 Info-Karten) und „MCP-Verbindung" (POST Streamable HTTP + GET SSE) Die Reihenfolge ist Absicht: wer die Startseite öffnet, will seine Notizen sehen, nicht erklärt bekommen, was Markdown ist. Das Onboarding bleibt vollständig erhalten, nimmt der täglichen Arbeit aber nicht den Platz. Das frühere Statuspanel (Format `.md`, Suche `FTS5`, Zugriff ``) ist entfallen — drei Fakten, die sich nie ändern und die Rolle steht ohnehin im Account-Menü. HTMX-Endpoints für Dashboard: - `/ui/recent-edited` → `partials/recent_edited.html` (sortiert nach `updated`-Frontmatter) - `/ui/recent-created` → `partials/recent_created.html` (sortiert nach `created`-Frontmatter) Sidebar (`{% block sidebar %}`): - Dateibaum mit Filter + Multi-Select Toolbar (write/admin) - Account-Menu Partial Der Dateibaum ist die Hauptnavigation und wird deshalb offen gerendert. Sein Zustand liegt im Cookie `kiwiki_sidebar` (`open`/`closed`), das `openSidebar()`/`closeSidebar()` im Desktop-Zweig setzen und `template_language_context()` als `sidebar_open` in jedes Template gibt. Damit rendert der Server gleich richtig — ohne Cookie wäre der Baum nach jedem Seitenwechsel wieder eingeklappt, und ein reines JS-Aufklappen nach `DOMContentLoaded` wäre als Sprung sichtbar. Auf Mobile hält CSS die Sidebar unabhängig davon geschlossen (`transform: translateX(-100%)`); `kwSyncSidebarForViewport()` setzt dort `inert`/`aria-hidden` nach. ## Partials (HTMX swaps) | Partial | Rendered by | Swapped into | |---|---|---| | `partials/file_tree.html` | `GET /ui/files?path=…` | `#file-tree` | | `partials/file_view.html` | `GET /ui/file?path=…` | `#main-content` | | `partials/search_results.html` | `POST /ui/search` | `#search-results` | | `partials/recent_edited.html` / `partials/recent_created.html` | `GET /ui/recent-edited`, `GET /ui/recent-created` | `#recent-edited`, `#recent-created` (mit `excerpt` + `tags` je Eintrag, max. 8, Dateien >100 KB ohne Ausschnitt) | | `partials/tag_cloud.html` | `GET /ui/tags?compact=1` | `.home-tags` (Top-12-Chips; leer → `""`, Sektion blendet sich per `:empty` aus) | | `partials/tags_overview.html` | `GET /ui/tags` | Subview mit Breadcrumb + Dateilisten | | `partials/sidebar_account.html` | included by `index.html` and `editor.html` | `.sidebar-account` | Partials must be self-contained — they cannot rely on `