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.
| Co | Jak |
|---|---|
| Backend | Rust (edition 2024), Axum 0.8, Tokio |
| Databáze | PostgreSQL přes SeaORM |
| Veřejné stránky | MiniJinja šablony renderované serverem |
| Admin UI | Vue 3 SPA (Pinia, Vue Router, Tailwind 4), embed přes rust-embed |
| Markdown | pulldown-cmark + custom direktivy (::page, ::img, ::file, ::gallery, ::fen, ::pgn) |
| Auth | Argon2, session cookies, service tokeny, OAuth2 (PKCE, RFC 7591) |
| AI | vlastní assistant s LLM provider configy, MCP klienty a tool permissions |
| MCP | server pro Claude — read/edit pages, tags, files, galleries |
| Nasazení | Docker + docker-compose |
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řesrust-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-subscribers 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:
- Inicializace
tracing(env filter zRUST_LOG). - Načtení
Configz prostředí (DATABASE_URL,PORT,NAMESPACE, …). - Vytvoření
AppState(DB pool + MiniJinja env naplněný zassets/<NAMESPACE>/templates/). - Spuštění SeaORM migrací (
Migrator::up) — automaticky. - Sestavení Axum routeru s podtřídami
mcp,oauth,public,api, admin SPA zrust-embed. - Listen na
0.0.0.0:$PORT.
Veřejné stránky — catch-all
Fallback handler GET /{*path} (viz src/routes/public/mod.rs):
- Hledá shodu v tabulce
menus→ renderuje její markdown. - Hledá shodu v tabulce
pages→ renderuje obsah stránky (skryté soukromé stránky pro nepřihlášené). - 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.
Datový model
Databáze je PostgreSQL, přístup přes SeaORM. Migrace m_001–m_022 v src/migration/ běží automaticky při startu serveru.
Uživatelé a auth
users
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
username | unique | Přihlašovací jméno |
password_hash | text | Argon2 hash |
tokens
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
nonce | unique | Hodnota tokenu |
user_id | FK → users | Vlastník |
expires_at | timestamp? | NULL = bez expirace |
label | text? | Lidský popis (u service tokenů) |
is_service | bool | false = session cookie (24 h), true = service token (manuální správa) |
oauth_clients
RFC 7591 — dynamicky registrovaní OAuth klienti.
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
client_id | unique | Identifikátor klienta |
client_secret | text? | Volitelné (PKCE-only klienti ho nemají) |
client_name | text | Display name |
redirect_uris | JSON | Seznam povolených redirect URI |
oauth_codes
Krátkodobé authorization codes (10 min, PKCE).
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
code | unique | Auth code |
client_id | text | |
user_id | FK → users | |
redirect_uri | text | |
code_challenge | text | SHA-256 PKCE challenge |
expires_at | timestamp | |
used | bool | One-time use |
oauth_tokens
Access (1 h) + refresh (bez expirace) tokeny.
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
access_token | unique | Bearer pro /mcp a další chráněné API |
refresh_token | unique | Pro grant_type=refresh_token |
client_id, user_id | ||
expires_at | timestamp | Pouze pro access |
revoked | bool |
Obsah
pages
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
path | unique | URL cesta (např. about/blog) |
summary | text? | Krátký popis |
markdown | text | Obsah stránky |
tag_ids | INT[] | Pole tagů |
private | bool | Skrytá pro nepřihlášené |
created_at/by, modified_at/by | Audit |
Migrace m_022 přidává fulltext index nad path + markdown (accent-insensitive) — pohání search_pages MCP tool i /search.
page_revisions
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
page_id | FK → pages | |
seq | int | Pořadí revize v rámci stránky |
prev_markdown | text | Stav před změnou |
diff | text | Diff (knihovna diffy) |
created_at/by | Audit |
tags
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
name | unique | Název tagu |
description | text? | Popis |
menus
Statický obsah pro top-level cesty (např. /about, /blog, /partie).
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
path | unique | URL cesta |
markdown | text | Obsah |
private | bool | Skrytá 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.
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
path | unique | Identifikátor / název (přidáno v m_017) |
hash | text | SHA-256 obsahu (FK → file_blobs.hash) |
mimetype | text | Např. image/png |
size_bytes | int | |
description | text? | |
created_at/by | Audit |
file_blobs
Obsah souboru, deduplikovaný hashem.
| Sloupec | Typ | Popis |
|---|---|---|
hash | PK | SHA-256 |
data | bytea | Binární obsah |
size_bytes | int | |
created_at |
file_thumbnails
Automaticky generované náhledy obrázků (knihovna image).
| Sloupec | Typ | Popis |
|---|---|---|
file_id | PK / FK → files | |
hash | text | Hash náhledu |
width, height | int | |
mimetype | text |
galleries
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
path | unique | (přidáno v m_020) |
title | text | |
description | text? | |
file_ids | INT[] | Seznam ID souborů, v pořadí zobrazení |
created_at/by | Audit |
AI assistant
Detail v about/blog/ai.
llm_providers
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
label | text | Lidský název |
kind | enum | anthropic, ollama, gemini |
api_key | text? | Pokud poskytovatel vyžaduje |
base_url | text? | Pro self-hosted endpointy |
llm_models
Po m_015 rozdělené z původní llm_providers.
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
provider_id | FK → llm_providers | |
label | text | Lidský název |
model | text | Wire ID (např. claude-opus-4-7) |
is_default | bool |
assistant_sessions
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
user_id | FK → users | |
title | text | |
provider, model | text | Snapshoty pro audit |
model_id | FK → llm_models? | |
enabled_mcp_server_ids | JSONB | Pole ID, které MCP servery jsou v session aktivní (od m_018) |
created_at, updated_at |
assistant_messages
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
session_id | FK → assistant_sessions | |
seq | int | Pořadí v session |
role | enum | user, assistant (případně tool) |
content | JSON | Multi-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.
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
user_id | FK → users | |
name | text | |
url | text | HTTP MCP endpoint |
enabled | bool | |
forward_user_token | bool | Předávat session token do MCP serveru |
headers | JSON | Vlastní hlavičky (např. Authorization) |
tool_permissions
Pravidla pro povolení/zákaz volání nástrojů AI assistentem.
| Sloupec | Typ | Popis |
|---|---|---|
id | PK | |
user_id | FK → users | |
name | text | Pattern (wildcard) jména nástroje |
effect | enum | allow, deny, prompt |
priority | int | Vyšší = vyhodnoceno dřív |
Routes a API
Aplikace má čtyři vrstvy routes:
- Veřejné — server-rendered HTML, fallback catch-all.
- Admin SPA — Vue 3 SPA z embedovaných assetů.
- JSON API
/api/*— vše, čím SPA komunikuje se serverem. - OAuth2 + MCP — strojový přístup pro Claude a další klienty.
Veřejné routes
| Path | Metoda | Popis |
|---|---|---|
/files/{hash} | GET | Plný soubor (content-addressed, cache-friendly) |
/files/{hash}/nahled | GET | Thumbnail |
/tag/{name} | GET | Stránky s daným tagem |
/search?q=… | GET | Fulltextové vyhledávání |
/sitemap.xml | GET | Sitemap |
/static/{*path} | GET | Namespaced statické assety (assets/<NAMESPACE>/{css,js,img}) |
/{*path} | GET | Catch-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
| Path | Metoda | Popis |
|---|---|---|
/admin | GET | Vstup do SPA — vrací index.html |
/admin/{*path} | GET | Statické 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í.
| Skupina | Routes |
|---|---|
| auth | POST /api/auth/login, POST /api/auth/logout, GET /api/auth/me |
| pages | CRUD (GET, POST, GET/:id, PUT/:id, DELETE/:id); GET /api/pages/paths; POST /api/pages/:id/revisions/:rev_id/restore |
| tags | CRUD |
| files | CRUD; POST je multipart upload (limit 50 MB) |
| galleries | CRUD; GET /api/galleries/paths |
| menu | CRUD |
| tokens | GET, POST, DELETE/:id (správa service tokenů) |
| markdown | POST /api/markdown/render (markdown → HTML s expanzí direktiv) |
| paths | POST /api/paths/children (enumerace složek + obsah) |
| assistant | Chat sessions, zprávy, streaming odpovědí (viz about/blog/ai) |
| llm | GET/POST /api/llm/providers, GET/POST /api/llm/models (CRUD) |
| tool-permissions | CRUD pravidel pro AI nástroje |
OAuth2 + MCP
| Path | Metoda | Popis |
|---|---|---|
POST /mcp | POST | MCP JSON-RPC 2.0 endpoint (Bearer auth) |
POST /oauth/register | POST | RFC 7591 dynamic client registration |
GET /oauth/authorize | GET | Login + consent stránka |
POST /oauth/authorize | POST | Vystaví auth code (10 min, PKCE) |
POST /oauth/token | POST | grant_type=authorization_code (PKCE verify) nebo refresh_token |
GET /.well-known/oauth-authorization-server | GET | Server metadata |
GET /.well-known/oauth-protected-resource | GET | Označuje /mcp jako chráněný resource |
Auth flow
| Klient | Mechanismus |
|---|---|
| Vue admin SPA | POST /api/auth/login → server nastaví cookie site_session (HTTP-only, Lax, 24 h) |
| Service skript / starý MCP klient | Service token z /admin/tokeny → Authorization: Bearer <token> |
| Claude Desktop / Claude.ai | OAuth2 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).
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ástroj | Popis |
|---|---|
search_pages | Začí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_page | Vrátí metadata + plný markdown stránky podle přesné cesty. |
edit_page | Vytvoří nebo aktualizuje stránku. Mění jen předaná pole; nová stránka je defaultně private. Diff revize se ukládá automaticky. |
delete_page | Smaže stránku podle cesty. |
Tagy
| Nástroj | Popis |
|---|---|
list_tags | Všechny tagy + popisy. Před přiřazením tagu si projdi seznam — jména jsou case-sensitive. |
read_tag | Detail jednoho tagu. |
create_tag | Nový tag. |
update_tag | Přejmenování / popis (lookup podle aktuálního jména). |
delete_tag | Smazat tag. |
Soubory
| Nástroj | Popis |
|---|---|
list_files | Seznam souborů, volitelně filtrovaný mime_prefix (např. image/). |
create_file | Upload — buď data_base64 (binárka) nebo data (raw text). U obrázků se thumbnail vygeneruje sám. Vrátí id. |
read_file | Metadata souboru (bez binárky). |
update_file | Přejmenování / popis. |
delete_file | Smazat soubor. |
Galerie
| Nástroj | Popis |
|---|---|
list_galleries | Všechny galerie. |
read_gallery | Detail galerie + seznam souborů. |
create_gallery | Nová galerie (path, title, description?, file_ids?). |
update_gallery | Aktualizace. |
delete_gallery | Smazat 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í
| Syntaxe | Vý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.
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_searchpřes Serper API; aktivní jen pokud je nastavenSERPER_API_KEY. - Tool permissions — pravidla
allow/deny/promptper 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_driverzpracovává agentic loop (model → tool calls → výsledky → další iterace) a zprávy proudí do UI.
Komponenty (src/ai/)
| Modul | Popis |
|---|---|
loop_driver.rs | Agentic smyčka — volá LLM, zpracuje tool calls, ukládá zprávy, streamuje výstup |
tool_registry.rs | Sjednocený 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.rs | Vyhodnocení 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.rs | AiConfig (env, defaulty) |
Datový model
| Tabulka | Role |
|---|---|
llm_providers | Provider (anthropic/ollama/gemini) + api_key + base_url |
llm_models | Konkrétní model pod providerem (např. claude-opus-4-7); jeden je is_default |
assistant_sessions | Session se snapshotem provideru a modelu, plus enabled_mcp_server_ids (JSONB) |
assistant_messages | Zprávy v session (role + JSON content pro multi-modální obsah) |
user_mcp_servers | Uživatelovy externí MCP servery (URL, hlavičky, forward_user_token) |
tool_permissions | Pravidla pro povolování nástrojů |
Detail sloupců: about/blog/databaze.
API
| Cesta | Popis |
|---|---|
/api/assistant/* | Sessions, zprávy, streaming odpovědí |
/api/llm/providers | CRUD providerů |
/api/llm/models | CRUD modelů |
/api/tool-permissions | CRUD 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í dollm_providers.api_keypřes admin UI — nikdy v kódu ani v.env. - Self-hosted Ollama — nastav
base_urlprovidera (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_tokenpropisuje uživatelův session token do hlaviček MCP serveru.
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á | Popis | Default |
|---|---|---|
DATABASE_URL | PostgreSQL connection string | (povinné — docker-compose nastavuje postgres://blog:blog@db:5432/blog) |
RUST_LOG | Log level filtr | site=debug,tower_http=debug,info |
PORT | HTTP listen port | 3000 |
NAMESPACE | Vybírá assets/<NAMESPACE>/{templates,css,js,img} bundle | common |
SERPER_API_KEY | Volitelné — 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 profilemfull:
# 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í.