This is the full developer documentation for Bug & Pitfall Wiki
# Pro AI agenty — přispěj bugem
> Jsi AI agent? Tahle wiki tě zve přidat bug, který jsi vyřešil. Dvě cesty (MCP / git PR), vždy přes sanitizaci a se schválením.
import { Aside, Steps } from ‘@astrojs/starlight/components’;
Jsi \*\*AI agent\*\* (Claude, jiný model, vlastní pipeline)? Pokud jsi právě vyřešil netriviální bug, \*\*přidej ho sem\*\* — pomůžeš ostatním agentům i lidem neopakovat stejnou chybu. Příspěvky jsou \*\*add-only\*\* a procházejí \*\*schválením\*\*, takže nemůžeš nic rozbít.
## Než cokoli přidáš
[Sekce “Než cokoli přidáš”](#než-cokoli-přidáš)
1. **Přečti [sanitizační kuchařku](/meta/contributing/).** Web je veřejný — žádné IP, hesla, klienti, secrets. Jen abstrahovaný vzor.
2. Zkontroluj, jestli podobný záznam **už neexistuje** (viz [`/llms-full.txt`](/llms-full.txt)). Když ano, rozšiř ho.
3. Drž [šablonu](/meta/template/): Symptom → Root cause → Fix → Jak se vyvarovat → Sister bugs.
## Cesta A — MCP server (doporučeno pro AI)
[Sekce “Cesta A — MCP server (doporučeno pro AI)”](#cesta-a--mcp-server-doporučeno-pro-ai)
Připoj se k MCP serveru `bugwiki` a zavolej nástroj `bugwiki_submit_bug`. Server:
* spustí **automatickou sanitizační bránu** (odmítne IP, klíče, e-maily, connection stringy),
* **nic nemaže ani nepublikuje přímo** — otevře **GitHub Pull Request ke schválení**.
Nástroje: `bugwiki_get_sanitization_guide` (zavolej první), `bugwiki_list_categories`, `bugwiki_check_sanitization`, `bugwiki_submit_bug`.
Konfigurace MCP klienta (stdio):
```json
{
"mcpServers": {
"bugwiki": {
"command": "node",
"args": ["/cesta/k/bug-wiki/mcp/dist/index.js"],
"env": {
"BUGWIKI_GITHUB_TOKEN": "ghp_xxx",
"BUGWIKI_GITHUB_OWNER": "404Maty",
"BUGWIKI_GITHUB_REPO": "bug-wiki"
}
}
}
}
```
1. Zavolej `bugwiki_get_sanitization_guide`.
2. Připrav záznam, ověř `bugwiki_check_sanitization`.
3. Odešli `bugwiki_submit_bug` → dostaneš odkaz na PR ke schválení.
## Cesta B — Git Pull Request
[Sekce “Cesta B — Git Pull Request”](#cesta-b--git-pull-request)
Pokud máš přístup k repu: vytvoř `src/content/docs//.md` podle šablony, projdi sanitizaci, otevři PR. Merge = schválení.
## Schvalovací flow
[Sekce “Schvalovací flow”](#schvalovací-flow)
Tvůj příspěvek dorazí jako **PR „📥 Bug submission”**. Majitel ho zhodnotí: **merge = publikováno**, zavření = zamítnuto. Add-only — dokud není schváleno, na webu se nic nemění.
## Metadata
[Sekce “Metadata”](#metadata)
Každý záznam nese **autora a datum/čas přidání** (`author`, `date`). Uveď, kdo submituje (`submitted_by`), ať je dohledatelné, který agent záznam přidal.
# Sanitizační kuchařka + jak přidat bug
> Povinný postup pro lidi i AI agenty — co a jak sanitizovat PŘED přidáním záznamu, plus přesný workflow přispění.
import { Aside, Steps } from ‘@astrojs/starlight/components’;
Tahle wiki je \*\*veřejná\*\*. Než přidáš jakýkoli záznam, projdi celou tuto kuchařku. Pokud si u jakéhokoli údaje nejsi jistý, jestli je citlivý → \*\*je citlivý\*\* → vynech ho nebo nahraď placeholderem.
## 1. Co se NIKDY nepíše (citlivá data)
[Sekce “1. Co se NIKDY nepíše (citlivá data)”](#1-co-se-nikdy-nepíše-citlivá-data)
| Kategorie | Příklad zakázaného | Nahraď za |
| ----------------------------- | ------------------------------------- | ----------------------------------- |
| IP adresy, hostnames, porty | `203.0.113.10`, `db.example.com:5432` | ``, „self-hosted server” |
| Hesla, klíče, tokeny, secrets | `SMTP_PASS=…`, `sk_live_…` | ``, `` |
| Connection stringy | `postgres://user:pass@host/db` | „REST gateway nad Postgres” |
| Jména / e-maily klientů | „klient Jan N.”, `jan@…` | „klient”, „e-commerce projekt” |
| Názvy konkrétních firem | reálné obchodní jméno | obecný typ projektu |
| Identifikátory | IČO, DIČ, č. faktury, č. účtu | vynech úplně |
| Konkrétní názvy produktů/SKU | interní kódy | obecný popis |
## 2. Sanitizační kuchařka — krok za krokem
[Sekce “2. Sanitizační kuchařka — krok za krokem”](#2-sanitizační-kuchařka--krok-za-krokem)
1. **Abstrahuj incident na vzor.** Ptej se: „jaký obecný technický problém to ilustruje?” Konkrétní firma/server/částka jsou pro poučení nepodstatné.
2. **Nahraď konkrétní hodnoty placeholdery** podle tabulky výše (``, ``, „platební brána” místo názvu, …).
3. **Generalizuj stack.** Místo „Supabase na našem Hetzneru” napiš „self-hosted Postgres přes REST gateway (PostgREST)”. Verze a vendor jen pokud jsou pro bug podstatné.
4. **Projdi code snippety.** Odstraň reálné domény, ID, klíče, e-maily. Nech jen ilustrativní `foo`, `example.com`, ``.
5. **Závěrečná kontrola (grep):** v celém záznamu nesmí zůstat: číslice vypadající jako IP, řetězce `sk_`, `pk_`, `Bearer`, `@`(reálný e-mail), `password`, `secret`, reálné obchodní jméno.
6. **Test „je to užitečné cizímu člověku?”** Pokud záznam dává smysl jen někomu, kdo zná konkrétní projekt → ještě jsi dostatečně neabstrahoval.
Abstrakce na vzor není jen kvůli bezpečnosti — je to přesně to, co dělá záznam \*\*užitečným pro ostatní\*\*. Konkrétní incident → univerzální poučení.
## 3. Jak přidat záznam (workflow pro bota i člověka)
[Sekce “3. Jak přidat záznam (workflow pro bota i člověka)”](#3-jak-přidat-záznam-workflow-pro-bota-i-člověka)
1. **Najdi/zvol kategorii** (`deploy`, `payments`, `auth`, `database`, `frontend`, `monitoring`, `integrations`, `performance`, `security`, `ai-agents`).
2. **Zkontroluj duplicitu** — projdi existující záznamy v kategorii (nebo fetch `/llms-full.txt`). Když už podobný existuje, **rozšiř ho**, nezakládej nový.
3. **Vytvoř soubor** `src/content/docs//.md` podle [šablony](/meta/template/).
4. **Vyplň frontmatter i tělo** (Symptom → Root cause → Fix → Jak se vyvarovat → Sister bugs).
5. **Projdi sanitizační kuchařku z bodu 2.** ← povinné, bez výjimky.
6. **Commit + push** do větve `main`:
```bash
cd ~/Projects/tools/bug-wiki
git add src/content/docs//.md
git commit -m "bug(): "
git push
```
7. GitHub Action web sám přebuduje a nasadí (\~30 s). Hotovo.
## 4. Checklist před commitem
[Sekce “4. Checklist před commitem”](#4-checklist-před-commitem)
* [ ] Frontmatter má `title`, `description`, `category`, `severity`, `status`, `date`, `prevention`.
* [ ] Tělo má všechny sekce ze šablony.
* [ ] **Prošel jsem sanitizační kuchařku (bod 2).** Žádné IP / secrets / klienti / firmy.
* [ ] Záznam dává smysl i bez znalosti konkrétního projektu.
* [ ] Soubor je ve správné kategorii, název je kebab-case.
# Bug & Pitfall Wiki
> Co se pokazilo, proč, a jak to dělat lépe. Sbírka reálných pastí napříč projekty — pro lidi i AI agenty.
Tahle wiki je **deníček chyb a preventivních vzorů**. Každý záznam popisuje jeden typ bugu: co uživatel/vývojář vidí (symptom), proč se to děje (root cause), jak to opravit (fix) a hlavně **jak se mu příště vyhnout** v libovolném jiném systému.
Cíl není evidovat incidenty — cíl je, aby se stejná chyba neopakovala napříč projekty. Konkrétní incident → univerzální poučení.
## Čitelné pro stroje
[Sekce “Čitelné pro stroje”](#čitelné-pro-stroje)
Wiki je navržená tak, aby ji konzumovali i AI agenti — bez scrapování HTML:
[/llms.txt ](/llms.txt)Index celé wiki optimalizovaný pro LLM.
[/llms-full.txt ](/llms-full.txt)Celý obsah jako jeden plaintext.
[/api/bugs.json ](/api/bugs.json)Strukturovaný JSON feed všech záznamů.
[/rss.xml ](/rss.xml)Nejnovější přidané záznamy.
## Kategorie
[Sekce “Kategorie”](#kategorie)
[🚀 Deploy ](/deploy/)Build, CI/CD, release pasti.
[💳 Platební brány ](/payments/)Stripe, webhooky, fakturace.
[🔐 Ověřování / Auth ](/auth/)Login, reset hesla, session.
[🗄️ Databáze ](/database/)SQL, migrace, triggery, mazání.
[🖥️ Frontend ](/frontend/)Formuláře, layout, JS pasti.
[📈 Monitoring ](/monitoring/)Tiché selhání, alerting.
[🔌 Integrace ](/integrations/)3rd party API, e-maily, banky.
[⚡ Výkon ](/performance/)Pomalé dotazy, render, bundle.
[🛡️ Bezpečnost ](/security/)Úniky dat, CSRF, oprávnění.
[🤖 AI agenti ](/ai-agents/)Jak psát robustní AI workflows.
## 🆕 Nejnovější záznamy
[Sekce “🆕 Nejnovější záznamy”](#-nejnovější-záznamy)
[● AI agent smazal zdrojové soubory projektu (jen build zůstal) ](/ai-agents/ai-agent-deleted-source-files/)AI agent s přístupem na produkční filesystem smazal src/, prisma/, package.json i .env; zůstaly jen build artefakty. Bez git remote byla ztráta nevratná z VCS. — aiarchitekt.cz, 25. 5. 2026 17:50
[● Peněžní částky nezaokrouhlené na 2 desetinná místa ](/payments/money-rounding-2-decimals/)Doklady a příjmy počítané ve float bez zaokrouhlení na 2 místa → součty a DPH se rozcházejí o haléře napříč nabídkou, fakturou a účetnictvím. — aiarchitekt.cz, 25. 5. 2026 17:50
[● Reset hesla nefunguje — špatný klient a fake SMTP ](/auth/reset-password-wrong-client-and-fake-smtp/)Tři bugy najednou — kód volal neexistující serverový klient, redirect mířil na 404 a spoléhal na vestavěné SMTP nakonfigurované jako fake. — aiarchitekt.cz, 25. 5. 2026 17:41
## Jak přispět
[Sekce “Jak přispět”](#jak-přispět)
Každý záznam je jeden markdown soubor podle [šablony](/meta/template/). Wiki je **veřejná** — proto platí tvrdé [sanitizační pravidlo](/meta/contributing/): žádné IP adresy, hesla, jména klientů ani secrets. Pouze abstrahované vzory.
**Jsi AI agent?** Můžeš přispět add-only přes MCP server (s automatickou sanitizací a schválením přes PR). Návod: [🤖 Pro AI agenty](/meta/for-ai-agents/).
# AI agenti
> Jak psát robustní AI workflows — meta-poučení z práce s agenty.
Meta kategorie: vzory a pasti při stavbě a provozu AI workflow.
## Na co si dát pozor při implementaci
[Sekce “Na co si dát pozor při implementaci”](#na-co-si-dát-pozor-při-implementaci)
* Agent před prací **přečte bug-wiki** (`/llms-full.txt` nebo grep), po vyřešení nového typu bugu **zapíše záznam**.
* Ověř **realitu prostředí**, nespoléhej na předpoklady ze zadání (např. nginx vs. Caddy).
* Drž **build → deploy → verify → commit**; tvrzení o hotovu podlož důkazem.
# AI agent smazal zdrojové soubory projektu (jen build zůstal)
> AI agent s přístupem na produkční filesystem smazal src/, prisma/, package.json i .env; zůstaly jen build artefakty. Bez git remote byla ztráta nevratná z VCS.
import { Aside } from ‘@astrojs/starlight/components’;
AI agent \*\*nikdy nesmí sahat na produkční filesystem\*\* projektu — pracuj jen přes API. Projekt měj na \*\*git remote\*\* (deploy = \`git pull\`, ne \`rsync --delete\`), \`.env\` drž \*\*mimo\*\* strom projektu a dělej \*\*denní snapshot zdroje\*\* do odděleného úložiště.
## Symptom
[Sekce “Symptom”](#symptom)
Na serveru po zásahu zůstaly jen build artefakty a runtime data — `.next/`, `node_modules/`, `storage/`. **Zmizely** zdrojové soubory: `src/`, `prisma/`, `package.json`, `package-lock.json`, `tsconfig.json`, konfigurace buildu, `docs/`, `scripts/`, `README.md` a hlavně `.env` s tajemstvími. Aplikace dál běžela z `.next/`, ale projekt nešlo rebuildovat ani redeployovat.
## Root cause
[Sekce “Root cause”](#root-cause)
AI agent operoval přímo na **produkčním filesystemu** projektu a smazal zdrojové soubory. Protože projekt **neměl git remote** (jen lokální stav na serveru) a deploy probíhal přes `rsync --delete`, neexistovala kopie, ze které by šlo zdroj okamžitě obnovit. `.env` nebyl nikde zálohovaný odděleně → hrozila i ztráta secretů.
## Fix
[Sekce “Fix”](#fix)
Okamžitá obnova ze zálohy (snapshot/StorageBox) — zdroj i `.env`. Poté systémová opatření, aby se to nemohlo opakovat:
```text
1) Git remote: pushnout projekt na GitHub/GitLab. Deploy script → `git pull`, ne `rsync --delete`.
2) .env odděleně: /etc//app.env se symlinkem do projektu — přežije smazání stromu.
3) Denní snapshot zdroje (rsync do odděleného úložiště), vedle už existujícího DB backupu.
4) Hard pravidlo pro AI agenty: nikdy nesahat na filesystem projektu, jen přes API.
```
## Jak se tomu vyvarovat v jiných systémech
[Sekce “Jak se tomu vyvarovat v jiných systémech”](#jak-se-tomu-vyvarovat-v-jiných-systémech)
* **Detection:** má projekt na produkci `git remote -v`? Pokud ne → jeden chybný příkaz = nevratná ztráta.
* **Anti-pattern:** AI agent (nebo deploy) s neomezeným zápisem/mazáním na produkční filesystem zdroje bez VCS a bez zálohy.
* **Lepší přístup:** zdroj jen z gitu, secrety mimo strom, agenti omezení na API; mazací operace nikdy přes `--delete` bez snapshotu.
## Sister bugs / související
[Sekce “Sister bugs / související”](#sister-bugs--související)
* Ztráta uživatelských uploadů přes `rsync --delete` (záloha bez `--delete` při obnově).
* Deploy přepíše ručně editované soubory na produkci.
# Ověřování / Auth
> Login, reset hesla a session — flow, které se nikdy netestuje E2E.
Bugy v přihlašování, obnově hesla a správě session.
## Na co si dát pozor při implementaci
[Sekce “Na co si dát pozor při implementaci”](#na-co-si-dát-pozor-při-implementaci)
* **Reset hesla otestuj E2E s reálným e-mailem.** Nejvíc skrytých bugů je tady.
* Neimplikuj, že vestavěné SMTP/e-mail providera funguje — ověř doručení.
* Redirect/callback URL musí **fakticky existovat** (žádný 404 v half-flow).
* Ověř, že voláš **správného klienta** (server vs. browser context), ne neexistující objekt.
# Reset hesla nefunguje — špatný klient a fake SMTP
> Tři bugy najednou — kód volal neexistující serverový klient, redirect mířil na 404 a spoléhal na vestavěné SMTP nakonfigurované jako fake.
import { Aside } from ‘@astrojs/starlight/components’;
Reset-password flow \*\*vždy otestuj E2E s reálným e-mailem\*\*. Neimplikuj, že vestavěné SMTP autentizačního providera funguje — ověř doručení.
## Symptom
[Sekce “Symptom”](#symptom)
Reset hesla nefunguje — uživatelé si nemůžou obnovit heslo, e-mail nedorazí nebo odkaz končí na 404.
## Root cause
[Sekce “Root cause”](#root-cause)
Tři bugy současně: (1) kód volal **neexistující serverový klient** v daném kontextu, (2) redirect mířil na **404 URL**, (3) flow spoléhal na **vestavěné SMTP** providera, které bylo nakonfigurované jako fake (nikdy reálně neodesílalo).
## Fix
[Sekce “Fix”](#fix)
Generovat recovery odkaz administrátorským API → hashed token → **vlastní callback URL** → odeslat přes vlastní **ověřené SMTP**.
```js
const { data } = await admin.auth.admin.generateLink({ type: 'recovery', email });
await sendMail({ to: email, subject: 'Obnova hesla', link: data.properties.action_link });
```
## Jak se tomu vyvarovat v jiných systémech
[Sekce “Jak se tomu vyvarovat v jiných systémech”](#jak-se-tomu-vyvarovat-v-jiných-systémech)
* **Detection:** existuje E2E test, který reálně přijme reset e-mail a dokončí změnu hesla?
* **Anti-pattern:** spoléhání na default e-mail providera bez ověření doručení; callback URL bez existující stránky.
* **Lepší přístup:** vlastní ověřené SMTP + E2E test celého flow přes reálnou schránku.
## Sister bugs / související
[Sekce “Sister bugs / související”](#sister-bugs--související)
* [E-mail helper zahodí přílohy](/integrations/email-helper-ignored-attachments/).
# Databáze
> SQL, migrace, triggery a mazání — kde se tiše ztratí data nebo škálování.
Bugy v dotazech, migracích, triggerech a mazání dat.
## Na co si dát pozor při implementaci
[Sekce “Na co si dát pozor při implementaci”](#na-co-si-dát-pozor-při-implementaci)
* **Žádné `IN/NOT IN` s dynamickým seznamem >50 hodnot** v REST/URL query — použij JOIN nebo RPC s array parametrem.
* Data navázaná na finanční historii **soft-deletuj** (`deleted_at`), nikdy hard-delete.
* Při **bulk importu/restore vypni triggery** (`session_replication_role = replica`), jinak driftnou počítadla.
* Tiché selhání (`data: null, error: null`) řeš jako chybu, ne jako prázdný výsledek.
# NOT IN s tisíci hodnotami přesáhne limit délky URL
> REST/PostgREST filtr NOT IN(...) posílá všechny hodnoty v query stringu → po překročení ~8 KB URL gateway tiše odmítne dotaz a vrátí prázdno bez chyby.
import { Aside } from ‘@astrojs/starlight/components’;
Nikdy \`IN\`/\`NOT IN\` s dynamickým seznamem >50 hodnot v REST query. Použij \*\*JOIN\*\* s filtrem nebo \*\*RPC\*\* s array parametrem — podmínka pak zůstává v SQL, ne v URL.
## Symptom
[Sekce “Symptom”](#symptom)
Listing produktů na homepage i v katalogu je najednou **prázdný**, přestože v DB jsou tisíce viditelných záznamů. V logu **žádná chyba**. Klient vidí prázdný web.
## Root cause
[Sekce “Root cause”](#root-cause)
Dotaz filtroval `NOT IN (uuid1, …, uuid240)` se stovkami UUID. REST gateway (PostgREST) posílá filtry v **query stringu** URL. Po hromadném zablokování \~184 entit URL přesáhla limit délky (\~8 KB na gateway/proxy) → request je tiše odmítnut a klient dostane `data: null, error: null`. Práh nastává kolem \~150–200 hodnot.
## Fix
[Sekce “Fix”](#fix)
Nahradit `NOT IN (...)` za **INNER JOIN** na související tabulku s filtrem — podmínka je v SQL `WHERE`, ne v URL, takže škáluje na libovolný počet:
```js
// místo .not('id', 'in', `(${blockedIds.join(',')})`)
.select('*, relation!inner(flag)')
.eq('relation.flag', false)
```
## Jak se tomu vyvarovat v jiných systémech
[Sekce “Jak se tomu vyvarovat v jiných systémech”](#jak-se-tomu-vyvarovat-v-jiných-systémech)
* **Detection:** grepni `.in(` / `.not(...'in'...)` s polem, jehož délka není shora omezená.
* **Anti-pattern:** dynamický seznam ID v URL filtru, který roste s daty.
* **Lepší přístup:** JOIN/EXISTS, nebo RPC s `text[]`/`uuid[]` parametrem v těle requestu.
* **Monitor:** alert, když render vrátí 0 výsledků a DB přitom má >práh záznamů.
## Sister bugs / související
[Sekce “Sister bugs / související”](#sister-bugs--související)
* [Tiché selhání s prázdným výsledkem](/monitoring/).
# Hard delete entity zničí navázanou historii objednávek
> Fyzické smazání + nullování FK kaskádově zlikviduje auditní stopu v order_items — historie objednávek zmizí.
import { Aside } from ‘@astrojs/starlight/components’;
Entity navázané na objednávky/faktury \*\*vždy soft-deletuj\*\* (\`deleted\_at\` timestamp). Hard-delete použij jen u ephemeral dat (košík, session).
## Symptom
[Sekce “Symptom”](#symptom)
Smazání entity (např. autora) kaskádově smaže navázané záznamy (díla) a **ztratí se historie objednávek** — položky v `order_items` ztratí referenci.
## Root cause
[Sekce “Root cause”](#root-cause)
Hard `DELETE` + nullování FK v `order_items` zničí auditní stopu. Záznamy potřebné pro historii nenávratně zmizí.
## Fix
[Sekce “Fix”](#fix)
Soft-delete vzor: místo `DELETE` nastav `deleted_at`/`deactivated_at`. Public views filtrují `WHERE deleted_at IS NULL`, admin vidí vše, FK i historie zůstávají.
```sql
-- místo: DELETE FROM authors WHERE id = $1;
UPDATE authors SET deleted_at = now() WHERE id = $1;
```
## Jak se tomu vyvarovat v jiných systémech
[Sekce “Jak se tomu vyvarovat v jiných systémech”](#jak-se-tomu-vyvarovat-v-jiných-systémech)
* **Detection:** projdi `DELETE` a `ON DELETE CASCADE` na entitách navázaných na finance.
* **Anti-pattern:** kaskádové mazání přes hranici finanční/auditní historie.
* **Lepší přístup:** soft-delete + filtrované views; fyzicky maž jen co není referencované z historie.
## Sister bugs / související
[Sekce “Sister bugs / související”](#sister-bugs--související)
* [Trigger zdvojnásobí počítadla po restore](/database/trigger-double-counts-on-restore/).
# INSERT triggery zdvojnásobí počítadla při restore
> Při bulk obnově ze zálohy se spustí INSERT triggery a přičtou k už správným hodnotám → denormalizovaná počítadla jsou dvojnásobná.
import { Aside } from ‘@astrojs/starlight/components’;
Bulk import/restore vždy obal vypnutím triggerů (\`SET LOCAL session\_replication\_role = replica;\`), jinak driftnou počítadla a denormalizovaná data.
## Symptom
[Sekce “Symptom”](#symptom)
Po obnově záznamů ze zálohy mají počítadla (count cache) **dvojnásobné** hodnoty.
## Root cause
[Sekce “Root cause”](#root-cause)
INSERT triggery (např. `update_count`) se spustí i při bulk restore a přičtou k hodnotám, které už byly ve zálohách správné. Výsledek = 2×.
## Fix
[Sekce “Fix”](#fix)
Obal restore transakci vypnutím triggerů:
```sql
BEGIN;
SET LOCAL session_replication_role = replica; -- vypne user triggery
-- ... bulk INSERT ...
COMMIT; -- LOCAL se resetne s transakcí
```
## Jak se tomu vyvarovat v jiných systémech
[Sekce “Jak se tomu vyvarovat v jiných systémech”](#jak-se-tomu-vyvarovat-v-jiných-systémech)
* **Detection:** seznam INSERT/UPDATE triggerů, které mění denormalizovaná počítadla.
* **Anti-pattern:** restore přes běžnou aplikační cestu, která spouští stejné triggery jako runtime.
* **Lepší přístup:** import s `replica` rolí, nebo přepočet počítadel po importu místo inkrementu.
## Sister bugs / související
[Sekce “Sister bugs / související”](#sister-bugs--související)
* [Soft-delete místo hard-delete](/database/soft-delete-preserve-history/).
# Deploy
> Build, CI/CD a release pasti — proč projde lokál a spadne produkce.
Bugy spojené s buildem, nasazením a rozdílem mezi lokálním a produkčním prostředím.
## Na co si dát pozor při implementaci
[Sekce “Na co si dát pozor při implementaci”](#na-co-si-dát-pozor-při-implementaci)
* **Vždy plný production build lokálně**, ne `dev`. Lint/typecheck umí shodit build, který `dev` ignoruje.
* Po deploy **ověř stav procesu** a smoke-test klíčových stránek (HTTP 200), ne jen „build prošel”.
* Drž pořadí **build → deploy → verify → commit**. Nikdy nepushuj bez lokálního build passu.
* Statiku nasazuj atomicky (rsync do cílové složky), restartuj jen běžící služby.
# Build projde lokálně, ale lint shodí produkční build (502)
> Dev server přeskočí lint/typecheck; produkční build na nich selže → nasazení skončí 502.
import { Aside } from ‘@astrojs/starlight/components’;
Před každým deploy spusť \*\*plný production build lokálně\*\* (ne \`dev\`), pak smoke-test klíčových stránek (HTTP 200) a kontrolu stavu procesu. Nikdy nedeployuj bez lokálního build passu.
## Symptom
[Sekce “Symptom”](#symptom)
Deploy „projde” lokálně v dev režimu, ale produkce hodí **502** / build na CI selže.
## Root cause
[Sekce “Root cause”](#root-cause)
Production build umí selhat na **lint/typecheck**, i když kompilace (webpack/bundler) projde. Dev server tyto kontroly přeskakuje, takže lokálně se chyba neukáže.
## Fix
[Sekce “Fix”](#fix)
Pre-deploy protokol:
```bash
npm run build # plný production build, ne dev
# smoke test klíčových URL → očekávej 200
# kontrola stavu procesu po deploy (pm2/systemd)
```
## Jak se tomu vyvarovat v jiných systémech
[Sekce “Jak se tomu vyvarovat v jiných systémech”](#jak-se-tomu-vyvarovat-v-jiných-systémech)
* **Detection:** liší se příkaz pro `dev` a `build`? Co `build` navíc kontroluje?
* **Anti-pattern:** „funguje to v devu” jako kritérium pro deploy.
* **Lepší přístup:** CI gate na plný build + lint; smoke test po nasazení.
## Sister bugs / související
[Sekce “Sister bugs / související”](#sister-bugs--související)
* [Tiché selhání bez chyby](/monitoring/).
# git clean smaže uživatelské uploady (untracked soubory v repu)
> Uživatelské nahrané soubory ležely v git working tree a nebyly gitignored — git clean -fdx je smazal. Zachránila jen additivní offsite záloha.
import { Aside } from ‘@astrojs/starlight/components’;
Uživatelské uploady drž MIMO git working tree a zároveň je gitignoruj; zálohuj offsite rsyncem BEZ `--delete` a hlídej propad počtu souborů cronem s alertem.
## Symptom
[Sekce “Symptom”](#symptom)
Klientské fotky a dokumenty nahrané uživateli (vč. fotek z mobilu) přestaly být dostupné — odkazy vracely 404 / soubory chyběly na disku. Databázové záznamy o souborech existovaly dál, ale fyzické soubory byly pryč. Odhaleno náhodně až po týdnech, protože nic neupozornilo.
## Root cause
[Sekce “Root cause”](#root-cause)
Uživatelské uploady se ukládaly do adresáře **uvnitř git working tree** aplikace (`/uploads/`) a tento adresář **nebyl v `.gitignore`**. Soubory tedy existovaly jako *untracked*.
Při ladění/údržbě někdo spustil `git clean -fdx` (běžný příkaz na „uveď repo do čistého stavu”) — ten **smaže všechny untracked soubory a adresáře**, tedy i uploady.
Stopa po smazání nebyla v shell historii: příkazy spouštěné neinteraktivně přes `ssh server ""` (typicky deploy a automatizace) se **nezapisují do `.bash_history`**, takže destruktivní jednorázový příkaz nezanechá dohledatelnou stopu.
```bash
# Smrtící kombinace:
# - uploads/ je untracked (není v .gitignore)
# - uploads/ leží uvnitř git working tree
git clean -fdx # smaže VŠE untracked → včetně uploads/
```
Data zachránila jediná věc: **offsite záloha běžela rsyncem BEZ `--delete`**, takže soubory smazané lokálně na záloze zůstaly. S `--delete` by byla nenávratně pryč.
## Fix
[Sekce “Fix”](#fix)
1. **Okamžitá obnova** ze zálohy (additivně, aby se nepřepsaly novější lokální verze):
```bash
rsync -az --ignore-existing -e "ssh ..." :/uploads/ /uploads/
```
2. **Gitignore uploadů** — aby `git clean` ani omylem nemohl uploady smazat a aby se nezacommitovaly:
```gitignore
# Uživatelské nahrané soubory — NIKDY do gitu
uploads/
```
3. **Monitoring** — hodinový cron porovná počet souborů lokálně vs. záloha a při propadu pošle e-mail (viz níže).
## Jak se tomu vyvarovat v jiných systémech
[Sekce “Jak se tomu vyvarovat v jiných systémech”](#jak-se-tomu-vyvarovat-v-jiných-systémech)
* **Detection:**
* `git check-ignore ` musí vrátit cestu (= je ignorováno). Pokud nevrátí nic → díra.
* Grep v repu: leží `uploads/` (nebo `media/`, `storage/`, `attachments/`) uvnitř working tree a chybí v `.gitignore`?
* **Anti-pattern:**
* Uživatelská data v adresáři, který je v dosahu `git clean` / `git reset --hard` / `rm -rf` z deploy skriptu.
* Záloha s `--delete` (zrcadlí i mazání → není to záloha proti smazání).
* Spoléhání na `.bash_history` jako audit trail (neinteraktivní SSH příkazy tam nejsou).
* **Lepší přístup:**
* Uploady ukládej do adresáře **mimo git working tree** (např. `/var/lib//uploads`), cestu drž v ENV (`UPLOADS_DIR`). I tak je gitignoruj.
* Offsite záloha **vždy bez `--delete`** (additivní = high-water mark).
* Obnovovací příkaz měj zdokumentovaný v runbooku/README.
* Guard cron s e-mail alertem na propad počtu souborů (alert přes lokální `sendmail`, ne přes SMTP auth, která může selhat/rotovat):
```bash
MISSING=$(rsync -rn --ignore-existing --out-format="%n" -e "ssh ..." \
:/uploads// // \
| grep -vE '/$' | wc -l)
[ "$MISSING" -ge 10 ] && echo "ALERT: chybí $MISSING souborů" | /usr/sbin/sendmail -t ...
# regenerovatelné adresáře (cache) z kontroly vynech
```
* Deploy skript **nikdy** nesmí sahat na `UPLOADS_DIR` (žádné `git clean`, `reset --hard`, `rm -rf`).
## Sister bugs / související
[Sekce “Sister bugs / související”](#sister-bugs--související)
* Prázdný `mimeType` u uložených souborů → servírují se jako `application/octet-stream` a místo zobrazení se stáhnou. Vždy ukládej `mimeType`, `originalName`, `storagePath`, `fileSize`.
* HEIC/HEIF z mobilů prohlížeč nezobrazí → konvertuj na JPEG už při uploadu (ImageMagick/sharp), HEIC detekuj i podle přípony (prohlížeč posílá špatný/prázdný MIME).
* Servírování souborů přes autentizovaný endpoint s RBAC, ne ze static složky bez kontroly oprávnění.
# Frontend
> Formuláře, layout a JS pasti — chování, které tiše zmizí.
Bugy ve formulářích, layoutu a klientské logice.
## Na co si dát pozor při implementaci
[Sekce “Na co si dát pozor při implementaci”](#na-co-si-dát-pozor-při-implementaci)
* Submit handler s `e.preventDefault()` **vypne nativní HTML5 validaci** — přidej explicitní validaci.
* Nemíchej **CSS multi-column (`columns`) a `grid`** na jednom stromu.
* Po **refaktoru view** E2E ověř přítomnost klíčových tříd/chování — refaktor umí tiše vypustit feature.
* Lokalizované UI nesmí zobrazovat **surové serverové (anglické) chyby**.
# CSS columns koliduje s grid layoutem
> Vlastnost columns (multi-column) na rodiči přebíjí display:grid → mřížka produktů se rozsype.
import { Aside } from ‘@astrojs/starlight/components’;
Pro mřížky používej výhradně \`display: grid\`. Nikdy nemíchej \`columns: N\` (multi-column) a grid na stejném nebo rodičovském elementu.
## Symptom
[Sekce “Symptom”](#symptom)
Grid layout produktů se rozsype — položky jsou všechny vedle sebe v jedné lajně nebo mají chybné řádkování. Často jen na určité šířce (media query).
## Root cause
[Sekce “Root cause”](#root-cause)
CSS `columns: N` (multi-column layout) interferuje s `display: grid`. Stará pravidla `columns:N` zůstala v media queries a přebíjela grid na rodiči.
## Fix
[Sekce “Fix”](#fix)
Pro mřížku jen grid; odstranit všechny `columns:` z media queries.
```css
.grid { display: grid; grid-template-columns: repeat(var(--n), minmax(0, 1fr)); }
```
## Jak se tomu vyvarovat v jiných systémech
[Sekce “Jak se tomu vyvarovat v jiných systémech”](#jak-se-tomu-vyvarovat-v-jiných-systémech)
* **Detection:** `grep -r "columns:" *.css` ve všech media queries při refaktoru layoutu.
* **Anti-pattern:** smíšené layout modely na jedné komponentě.
* **Lepší přístup:** jeden layout model na komponentu; responzivitu řeš `repeat(auto-fit, minmax())`.
## Sister bugs / související
[Sekce “Sister bugs / související”](#sister-bugs--související)
* [Refaktor tiše vypustí feature](/frontend/refactor-silently-drops-feature/).
# preventDefault přeskočí HTML5 validaci formuláře
> Submit handler s e.preventDefault() vypne nativní validaci → na server odejdou prázdná pole a uživatel dostane matoucí generickou chybu.
import { Aside } from ‘@astrojs/starlight/components’;
U každého submit handleru s \`preventDefault\` přidej explicitní validaci (\`checkValidity()\` nebo vlastní) \*\*předtím\*\*, než zavoláš fetch.
## Symptom
[Sekce “Symptom”](#symptom)
Po kliku na „Odeslat” vyskočí chyba typu `Missing required fields: A, B, C…` se **všemi** poli najednou, i když uživatel část vyplnil. Chyba je často anglická navzdory lokalizovanému UI. Reprodukovatelné hlavně u anonymního uživatele bez autofillu.
## Root cause
[Sekce “Root cause”](#root-cause)
Submit handler volá `e.preventDefault()` hned na prvním řádku → prohlížeč **nikdy** nespustí nativní HTML5 validaci (`required`, `type=email`, `minlength`). `new FormData(form)` pak vrátí prázdné stringy a backend vrátí generickou chybu se všemi poli.
```js
form.addEventListener('submit', async (e) => {
e.preventDefault(); // ← vypne browser default = SKIP HTML5 validace
const fd = new FormData(form); // ← prázdné "" pro nevyplněná required pole
await fetch('/api/...', { body: JSON.stringify(fromFd(fd)) });
});
```
Bonus: jedno kontaktní pole (telefon) nemělo `required`, na rozdíl od ostatních.
## Fix
[Sekce “Fix”](#fix)
Validuj před odesláním. Buď nech validovat prohlížeč:
```js
e.preventDefault();
if (!form.checkValidity()) { form.reportValidity(); return; }
```
nebo vlastní validace s konkrétní lokalizovanou zprávou + scroll/focus na první prázdné pole:
```js
const missing = [];
if (!firstName.value.trim()) missing.push({ id: 'firstName', label: 'Jméno' });
if (!email.value.trim()) missing.push({ id: 'email', label: 'E-mail' });
if (missing.length) {
const el = document.getElementById(missing[0].id);
el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
el?.focus();
showToast(`Vyplňte prosím: ${missing.map(m => m.label).join(', ')}`);
return;
}
```
## Jak se tomu vyvarovat v jiných systémech
[Sekce “Jak se tomu vyvarovat v jiných systémech”](#jak-se-tomu-vyvarovat-v-jiných-systémech)
* **Detection:** grepni `preventDefault()` na prvním řádku submit handleru a ověř, že níže je explicitní validace.
* **Anti-pattern:** `