about/blog collapse

Jak funguje tento blog

Tento blog je hybrid — server-rendered veřejné stránky v Rustu plus Vue 3 SPA admin zabudovaná do binárky. Žádný WordPress, žádný statický generátor.

Zdrojový kód: github.com/xmiksay/site.

CoJak
BackendRust (edition 2024), Axum 0.8, Tokio
DatabázePostgreSQL přes SeaORM
Veřejné stránkyMiniJinja šablony renderované serverem
Admin UIVue 3 SPA (Pinia, Vue Router, Tailwind 4), embed přes rust-embed
Markdownpulldown-cmark + custom direktivy (::page, ::img, ::file, ::gallery, ::fen, ::pgn)
AuthArgon2, session cookies, service tokeny, OAuth2 (PKCE, RFC 7591)
AIvlastní assistant s LLM provider configy, MCP klienty a tool permissions
MCPserver pro Claude — read/edit pages, tags, files, galleries
NasazeníDocker + docker-compose

about/blog/architektura collapse

Architektura a tech stack

Tech stack

  • Backend: Rust (edition 2024), Axum 0.8, Tokio async runtime
  • Databáze: PostgreSQL přes SeaORM 1.x; migrace běží automaticky při startu serveru
  • Veřejné stránky: MiniJinja šablony renderované serverem (loader z assets/<NAMESPACE>/templates/)
  • Admin UI: Vue 3 SPA (Pinia, Vue Router, Tailwind 4, Vite, TypeScript), buildovaná do client/dist/ a embedovaná do binárky přes rust-embed
  • Markdown: pulldown-cmark s vlastními direktivami (transclusion, obrázky, galerie, šachové pozice/partie)
  • Auth: Argon2 hashování hesel, session cookies pro SPA, service tokeny (legacy) a OAuth2 (PKCE, RFC 7591) pro API/MCP klienty
  • Logging: tracing + tracing-subscriber s env filtrem
  • AI: vlastní assistant podsystém v src/ai/ (více v about/blog/ai)

Struktura projektu

src/
  bin/
    site_server.rs       # Hlavní HTTP server (port 3000)
    site_migration.rs    # Migration CLI (up/down/fresh/status)
    site_cli.rs          # Admin CLI (create-user, change-password)
  routes/
    public/              # Veřejné routes (catch-all, files, search, sitemap, tags)
    api/                 # JSON API (auth, pages, tags, files, galleries,
                         #            menu, tokens, markdown, paths,
                         #            assistant, llm, tool-permissions)
    mcp.rs               # MCP JSON-RPC endpoint pro Claude
    oauth.rs             # OAuth2 server (register, authorize, token, well-known)
    revision.rs          # Revize stránek
  entity/                # SeaORM entity modely
    user, token, page, page_revision, tag, menu,
    file, file_blob, file_thumbnail, gallery,
    oauth_{client,code,token},
    llm_{provider,model},
    assistant_{session,message},
    user_mcp_server, tool_permission
  migration/             # m_001 … m_022 (vč. fulltext indexu, OAuth, AI tabulek)
  ai/                    # Assistant: loop_driver, tool_registry, mcp_client,
                         # tool_permissions, local_tools, llm, handlers, config
  auth.rs assets.rs config.rs files.rs
  markdown.rs path_util.rs repo state.rs

client/ # Vue 3 SPA — Pinia, Vue Router, Tailwind 4, Vite src/ # Komponenty, views, stores, router dist/ # Build output (embedovaný do binárky)

assets/<NAMESPACE>/ # Multi-tenant statické bundly css/ js/ img/ templates/ # MiniJinja šablony pro veřejné stránky

Jak server funguje

Při spuštění site_server proběhne:

  1. Inicializace tracing (env filter z RUST_LOG).
  2. Načtení Config z prostředí (DATABASE_URL, PORT, NAMESPACE, …).
  3. Vytvoření AppState (DB pool + MiniJinja env naplněný z assets/<NAMESPACE>/templates/).
  4. Spuštění SeaORM migrací (Migrator::up) — automaticky.
  5. Sestavení Axum routeru s podtřídami mcp, oauth, public, api, admin SPA z rust-embed.
  6. Listen na 0.0.0.0:$PORT.

Veřejné stránky — catch-all

Fallback handler GET /{*path} (viz src/routes/public/mod.rs):

  1. Hledá shodu v tabulce menus → renderuje její markdown.
  2. Hledá shodu v tabulce pages → renderuje obsah stránky (skryté soukromé stránky pro nepřihlášené).
  3. Jinak 404.

Markdown se renderuje custom direktivami (viz about/blog/mcp) a obalí se MiniJinja šablonou path_page.html.

Admin SPA

Routes GET /admin a GET /admin/{*path} čtou statické soubory z embedu client/dist/. Pokud cesta neexistuje (typický SPA hluboký link), server vrací index.html a Vue Router se postará o zbytek. Admin UI komunikuje výhradně přes JSON API /api/* — žádné serverové šablony.

Revize a diffy

Při každém uložení stránky se automaticky uloží řádek do page_revisions s diffem oproti předchozí verzi (knihovna diffy). Šetří místo, zachovává historii a dovoluje rollback přes POST /api/pages/:id/revisions/:rev_id/restore.


about/blog/databaze collapse

Datový model

Databáze je PostgreSQL, přístup přes SeaORM. Migrace m_001m_022 v src/migration/ běží automaticky při startu serveru.

Uživatelé a auth

users

SloupecTypPopis
idPK
usernameuniquePřihlašovací jméno
password_hashtextArgon2 hash

tokens

SloupecTypPopis
idPK
nonceuniqueHodnota tokenu
user_idFK → usersVlastník
expires_attimestamp?NULL = bez expirace
labeltext?Lidský popis (u service tokenů)
is_serviceboolfalse = session cookie (24 h), true = service token (manuální správa)

oauth_clients

RFC 7591 — dynamicky registrovaní OAuth klienti.

SloupecTypPopis
idPK
client_iduniqueIdentifikátor klienta
client_secrettext?Volitelné (PKCE-only klienti ho nemají)
client_nametextDisplay name
redirect_urisJSONSeznam povolených redirect URI

oauth_codes

Krátkodobé authorization codes (10 min, PKCE).

SloupecTypPopis
idPK
codeuniqueAuth code
client_idtext
user_idFK → users
redirect_uritext
code_challengetextSHA-256 PKCE challenge
expires_attimestamp
usedboolOne-time use

oauth_tokens

Access (1 h) + refresh (bez expirace) tokeny.

SloupecTypPopis
idPK
access_tokenuniqueBearer pro /mcp a další chráněné API
refresh_tokenuniquePro grant_type=refresh_token
client_id, user_id
expires_attimestampPouze pro access
revokedbool

Obsah

pages

SloupecTypPopis
idPK
pathuniqueURL cesta (např. about/blog)
summarytext?Krátký popis
markdowntextObsah stránky
tag_idsINT[]Pole tagů
privateboolSkrytá pro nepřihlášené
created_at/by, modified_at/byAudit

Migrace m_022 přidává fulltext index nad path + markdown (accent-insensitive) — pohání search_pages MCP tool i /search.

page_revisions

SloupecTypPopis
idPK
page_idFK → pages
seqintPořadí revize v rámci stránky
prev_markdowntextStav před změnou
difftextDiff (knihovna diffy)
created_at/byAudit

tags

SloupecTypPopis
idPK
nameuniqueNázev tagu
descriptiontext?Popis

menus

Statický obsah pro top-level cesty (např. /about, /blog, /partie).

SloupecTypPopis
idPK
pathuniqueURL cesta
markdowntextObsah
privateboolSkrytá pro nepřihlášené (od m_008)

Soubory a galerie

Starý systém images + gallery_images byl v m_010 nahrazen content-addressed schématem (deduplikace přes SHA-256).

files

Metadata souboru.

SloupecTypPopis
idPK
pathuniqueIdentifikátor / název (přidáno v m_017)
hashtextSHA-256 obsahu (FK → file_blobs.hash)
mimetypetextNapř. image/png
size_bytesint
descriptiontext?
created_at/byAudit

file_blobs

Obsah souboru, deduplikovaný hashem.

SloupecTypPopis
hashPKSHA-256
databyteaBinární obsah
size_bytesint
created_at

file_thumbnails

Automaticky generované náhledy obrázků (knihovna image).

SloupecTypPopis
file_idPK / FK → files
hashtextHash náhledu
width, heightint
mimetypetext

galleries

SloupecTypPopis
idPK
pathunique(přidáno v m_020)
titletext
descriptiontext?
file_idsINT[]Seznam ID souborů, v pořadí zobrazení
created_at/byAudit

AI assistant

Detail v about/blog/ai.

llm_providers

SloupecTypPopis
idPK
labeltextLidský název
kindenumanthropic, ollama, gemini
api_keytext?Pokud poskytovatel vyžaduje
base_urltext?Pro self-hosted endpointy

llm_models

Po m_015 rozdělené z původní llm_providers.

SloupecTypPopis
idPK
provider_idFK → llm_providers
labeltextLidský název
modeltextWire ID (např. claude-opus-4-7)
is_defaultbool

assistant_sessions

SloupecTypPopis
idPK
user_idFK → users
titletext
provider, modeltextSnapshoty pro audit
model_idFK → llm_models?
enabled_mcp_server_idsJSONBPole ID, které MCP servery jsou v session aktivní (od m_018)
created_at, updated_at

assistant_messages

SloupecTypPopis
idPK
session_idFK → assistant_sessions
seqintPořadí v session
roleenumuser, assistant (případně tool)
contentJSONMulti-modální obsah (text, tool calls, results)
created_at

user_mcp_servers

Externí MCP servery, které si uživatel přidá do své AI session.

SloupecTypPopis
idPK
user_idFK → users
nametext
urltextHTTP MCP endpoint
enabledbool
forward_user_tokenboolPředávat session token do MCP serveru
headersJSONVlastní hlavičky (např. Authorization)

tool_permissions

Pravidla pro povolení/zákaz volání nástrojů AI assistentem.

SloupecTypPopis
idPK
user_idFK → users
nametextPattern (wildcard) jména nástroje
effectenumallow, deny, prompt
priorityintVyšší = vyhodnoceno dřív

about/blog/api collapse

Routes a API

Aplikace má čtyři vrstvy routes:

  1. Veřejné — server-rendered HTML, fallback catch-all.
  2. Admin SPA — Vue 3 SPA z embedovaných assetů.
  3. JSON API /api/* — vše, čím SPA komunikuje se serverem.
  4. OAuth2 + MCP — strojový přístup pro Claude a další klienty.

Veřejné routes

PathMetodaPopis
/files/{hash}GETPlný soubor (content-addressed, cache-friendly)
/files/{hash}/nahledGETThumbnail
/tag/{name}GETStránky s daným tagem
/search?q=…GETFulltextové vyhledávání
/sitemap.xmlGETSitemap
/static/{*path}GETNamespaced statické assety (assets/<NAMESPACE>/{css,js,img})
/{*path}GETCatch-all — viz níže

Catch-all logika

GET /{*path}
  1. match v menus  → render menu markdown (skip pokud private a host nepřihlášen)
  2. match v pages  → render stránky    (skip pokud private a host nepřihlášen)
  3. → 404

Admin SPA

PathMetodaPopis
/adminGETVstup do SPA — vrací index.html
/admin/{*path}GETStatické soubory z client/dist/ přes rust-embed; chybějící cesty → fallback na index.html (deep linky pro Vue Router)

SPA pokrývá: stránky, tagy, soubory, galerie, menu, tokeny, AI assistant chat, LLM providery a modely, vlastní MCP servery, tool permissions.

JSON API /api/*

Vyžaduje session cookie site_session (HTTP-only, Lax, 24 h). Middleware require_login_api vrací 401 JSON, pokud chybí.

SkupinaRoutes
authPOST /api/auth/login, POST /api/auth/logout, GET /api/auth/me
pagesCRUD (GET, POST, GET/:id, PUT/:id, DELETE/:id); GET /api/pages/paths; POST /api/pages/:id/revisions/:rev_id/restore
tagsCRUD
filesCRUD; POST je multipart upload (limit 50 MB)
galleriesCRUD; GET /api/galleries/paths
menuCRUD
tokensGET, POST, DELETE/:id (správa service tokenů)
markdownPOST /api/markdown/render (markdown → HTML s expanzí direktiv)
pathsPOST /api/paths/children (enumerace složek + obsah)
assistantChat sessions, zprávy, streaming odpovědí (viz about/blog/ai)
llmGET/POST /api/llm/providers, GET/POST /api/llm/models (CRUD)
tool-permissionsCRUD pravidel pro AI nástroje

OAuth2 + MCP

PathMetodaPopis
POST /mcpPOSTMCP JSON-RPC 2.0 endpoint (Bearer auth)
POST /oauth/registerPOSTRFC 7591 dynamic client registration
GET /oauth/authorizeGETLogin + consent stránka
POST /oauth/authorizePOSTVystaví auth code (10 min, PKCE)
POST /oauth/tokenPOSTgrant_type=authorization_code (PKCE verify) nebo refresh_token
GET /.well-known/oauth-authorization-serverGETServer metadata
GET /.well-known/oauth-protected-resourceGETOznačuje /mcp jako chráněný resource

Auth flow

KlientMechanismus
Vue admin SPAPOST /api/auth/login → server nastaví cookie site_session (HTTP-only, Lax, 24 h)
Service skript / starý MCP klientService token z /admin/tokenyAuthorization: Bearer <token>
Claude Desktop / Claude.aiOAuth2 PKCE flow → access_token (1 h) + refresh_token (bez expirace) → Authorization: Bearer <access_token>

Hesla hashována Argon2 (src/auth.rs). MCP handler odvozuje user_id z bearer tokenu pro audit pole (created_by, modified_by).


about/blog/mcp collapse

MCP integrace (Claude)

Blog vystavuje MCP server (Model Context Protocol) na endpointu POST /mcp. Implementuje plné MCP přes JSON-RPC 2.0 a umožňuje Claudovi číst a editovat obsah blogu přímo z konverzace — přesně tak, jak vznikla tato stránka.

Dostupné nástroje

Stránky

NástrojPopis
search_pagesZačínej tímhle. Filtr přes prefix, tag a/nebo q (fulltext, accent-insensitive). Stránka prefix/tag rankuje výš než match v markdownu. Stránkování limit + offset.
read_pageVrátí metadata + plný markdown stránky podle přesné cesty.
edit_pageVytvoří nebo aktualizuje stránku. Mění jen předaná pole; nová stránka je defaultně private. Diff revize se ukládá automaticky.
delete_pageSmaže stránku podle cesty.

Tagy

NástrojPopis
list_tagsVšechny tagy + popisy. Před přiřazením tagu si projdi seznam — jména jsou case-sensitive.
read_tagDetail jednoho tagu.
create_tagNový tag.
update_tagPřejmenování / popis (lookup podle aktuálního jména).
delete_tagSmazat tag.

Soubory

NástrojPopis
list_filesSeznam souborů, volitelně filtrovaný mime_prefix (např. image/).
create_fileUpload — buď data_base64 (binárka) nebo data (raw text). U obrázků se thumbnail vygeneruje sám. Vrátí id.
read_fileMetadata souboru (bez binárky).
update_filePřejmenování / popis.
delete_fileSmazat soubor.

Galerie

NástrojPopis
list_galleriesVšechny galerie.
read_galleryDetail galerie + seznam souborů.
create_galleryNová galerie (path, title, description?, file_ids?).
update_galleryAktualizace.
delete_gallerySmazat galerii.

SERVER_INSTRUCTIONS — stránka CLAUDE

Instrukce, které Claude vidí při každém MCP handshaku, nejsou hardcoded v kódu, ale uložené jako privátní stránka na cestě CLAUDE. Server ji načte a předá jako SERVER_INSTRUCTIONS. Editace přes admin UI nebo edit_page — bez recompilu.

Pokud stránka neexistuje, použije se výchozí text z src/routes/mcp.rs.

Transclusion a read_page

Direktiva ::page{path=cesta/ke/stránce} slouží k renderování — server při výstupu do HTML vloží obsah odkazované stránky inline. Z pohledu Clauda jde ale o pouhý textový token; obsah odkazované stránky v markdownu není automaticky přítomen.

Pokud Claude narazí na ::page{path=some/page} a potřebuje znát její obsah, musí si sám zavolat read_page("some/page").

Auth

MCP routes přijímají Authorization: Bearer <token>, kde tokenem může být:

  • OAuth2 access token — vystavený PKCE flow (Claude Desktop, Claude.ai); platnost 1 h, obnovitelný refresh tokenem.
  • Service token (legacy) — vytvořený přes admin UI v sekci tokeny; bez expirace.

V obou případech MCP handler odvodí user_id pro audit pole (created_by, modified_by).

Markdown rozšíření

SyntaxeVýsledek
::page{path=…} nebo ::page{id=N}Vloží renderovaný obsah jiné stránky
::img{path|id|hash=…, alt=…}Obrázek s odkazem na plnou velikost a popiskem
::file{path|id|hash=…}Soubor — u obrázků inline, jinak download link
::gallery{path|id=…}Mřížka náhledů galerie
::fen{path|id|hash=…, size=small|large}Statická šachová pozice (FEN)
::pgn{path|id|hash=…, move=N, size=small|large}Přehrávatelná šachová partie (PGN)

Implementováno v src/markdown.rs.


about/blog/ai collapse

AI assistant

Server obsahuje vlastní AI assistant podsystém (src/ai/). Umožňuje psát chatovat s libovolným LLM (Anthropic, Ollama, Gemini), volat nástroje z interních MCP toolů i uživatelem připojených MCP serverů a přepínat mezi modely a sessions přímo v admin UI.

Co umí

  • Chat s LLM — multi-turn konverzace s plnou historií. Provider i model se volí na úrovni session a uloží se ve snapshotu pro audit.
  • Nástroje (tool calling) — assistant má přístup ke stejným nástrojům jako Claude přes MCP (read_page, edit_page, …) plus libovolné externí MCP servery, které si uživatel přidá.
  • Web search — lokální nástroj web_search přes Serper API; aktivní jen pokud je nastaven SERPER_API_KEY.
  • Tool permissions — pravidla allow / deny / prompt per uživatel a per nástroj (s wildcard patterny a prioritou). Uživatel rozhoduje, co může assistant volat bez potvrzení.
  • Streamování odpovědíloop_driver zpracovává agentic loop (model → tool calls → výsledky → další iterace) a zprávy proudí do UI.

Komponenty (src/ai/)

ModulPopis
loop_driver.rsAgentic smyčka — volá LLM, zpracuje tool calls, ukládá zprávy, streamuje výstup
tool_registry.rsSjednocený registr lokálních nástrojů a všech aktivních MCP serverů
mcp_client/Klient pro uživatelem připojené MCP servery (HTTP transport, persistentní spojení)
tool_permissions.rsVyhodnocení pravidel allow/deny/prompt podle priority
local_tools/Vestavěné nástroje (web search)
llm/Adaptéry pro anthropic, ollama, gemini
handlers/API handlery pro /api/assistant/*
config.rsAiConfig (env, defaulty)

Datový model

TabulkaRole
llm_providersProvider (anthropic/ollama/gemini) + api_key + base_url
llm_modelsKonkrétní model pod providerem (např. claude-opus-4-7); jeden je is_default
assistant_sessionsSession se snapshotem provideru a modelu, plus enabled_mcp_server_ids (JSONB)
assistant_messagesZprávy v session (role + JSON content pro multi-modální obsah)
user_mcp_serversUživatelovy externí MCP servery (URL, hlavičky, forward_user_token)
tool_permissionsPravidla pro povolování nástrojů

Detail sloupců: about/blog/databaze.

API

CestaPopis
/api/assistant/*Sessions, zprávy, streaming odpovědí
/api/llm/providersCRUD providerů
/api/llm/modelsCRUD modelů
/api/tool-permissionsCRUD pravidel
(uživatelské MCP servery)Spravované přes /api/assistant/* (případně dedikovaný endpoint)

Frontend stránky v admin SPA: /admin/providers, /admin/models, /admin/assistant, /admin/mcp-servers, /admin/tool-permissions.

Konfigurace

  • Klíče k poskytovatelům (anthropic, gemini, …) se ukládají do llm_providers.api_key přes admin UI — nikdy v kódu ani v .env.
  • Self-hosted Ollama — nastav base_url providera (např. http://localhost:11434).
  • SERPER_API_KEY — env proměnná. Pokud není nastavena, web search nástroj se zaregistruje jako nedostupný.
  • MCP servery — přidává si je každý uživatel sám přes UI (URL + hlavičky); forward_user_token propisuje uživatelův session token do hlaviček MCP serveru.

about/blog/provoz collapse

Provoz a nasazení

Požadavky

  • Rust (edition 2024)
  • Node.js (build Vue klienta — Dockerfile používá Node image)
  • PostgreSQL
  • Docker + Docker Compose (pro kontejnerizované nasazení)

Proměnné prostředí

ProměnnáPopisDefault
DATABASE_URLPostgreSQL connection string(povinné — docker-compose nastavuje postgres://blog:blog@db:5432/blog)
RUST_LOGLog level filtrsite=debug,tower_http=debug,info
PORTHTTP listen port3000
NAMESPACEVybírá assets/<NAMESPACE>/{templates,css,js,img} bundlecommon
SERPER_API_KEYVolitelné — zapne web search nástroj v AI assistantovi(nenastaveno)

Lokální vývoj

# 1) Build Vue klienta — embeduje se do binárky
cd client
npm ci
npm run build
cd ..

2) Spustit server (vyžaduje DATABASE_URL v .env nebo env)

cargo run --bin site_server

Ověření kompilace bez spuštění

cargo check

Server poslouchá na http://localhost:3000. Migrace proběhnou automaticky při startu.

Pro práci na klientovi bez restartu serveru lze spustit Vite dev server v client/ (npm run dev); buildovaná verze v client/dist/ se ale embeduje až při kompilaci serverové binárky.

Vytvoření uživatele

# Bez Dockeru
cargo run --bin site_cli -- create-user <username> <password>
cargo run --bin site_cli -- change-password <username> <password>

S Docker Compose

docker compose exec app ./site_cli create-user <username> <password>

Migrace

Migrace běží automaticky při startu serveru (Migrator::up). Ruční správa:

cargo run --bin site_migration              # apply all pending
cargo run --bin site_migration -- down      # rollback last
cargo run --bin site_migration -- fresh     # reset & reapply vše
cargo run --bin site_migration -- status    # zobraz stav

S Docker Compose

docker compose exec app ./site_migration

Docker Compose

docker-compose.yml definuje dvě služby:

  • db — PostgreSQL 17-alpine, navenek na portu 5434, interně 5432. DB jméno/user/heslo: blog/blog/blog.
  • app — multi-stage build (Node → Rust → debian-slim runtime). Spouští se jen s profilem full:
# Jen databáze
docker compose up -d db

Vše včetně aplikace

docker compose --profile full up -d --build

Docker image (samostatně)

docker build -t site .
docker run -e DATABASE_URL=postgres://… -p 3000:3000 site

Dockerfile provádí dvoufázový build: nejdřív Node stage (npm ci && npm run build v client/), pak Rust stage (cargo build --release), nakonec runtime image s binárkami site_server, site_migration, site_cli.

CI/CD

Repozitář obsahuje .github/workflows/ — GitHub Actions pipeline pro automatický build a nasazení.