Refaktor v4 — systémová architektura, hranice a dlouhodobá udržitelnost
Stav dokumentu: Částečně — první S0/S1 várka hotová;
3.1má první S2 řez (herb-detail-core.server.ts+ Vitest) bez změny SQL;4.1má loader kontrakty pro/byliny/:sluga/sezona/:month;4.2má první 404 helper řez pro dokumentační detail;5.1má první UI extrakci v katalogovém filtru. Dokument navazuje na hotový bezpečný rozsah refaktor-v3.md a archiv refaktor-v2.md.
Cíl: posunout aplikaci z dobře uklizených souborů na dlouhodobě udržitelnou architekturu: jasné doménové hranice, stabilní route contracts, testovatelné DB read modely, opakovatelné smoke scénáře a dokumentovaný proces změn.
Zásada: žádný big bang. Každý krok musí být malý, revertovatelný, měřitelný a ověřený přes npm run verify. U katalogových filtrů navíc npm run smoke:byliny-matrix. U UI kroků před/po screenshot nebo ruční vizuální kontrola konkrétní URL.
0. Kontext po v3
Refaktor v3 už dokončil většinu mechanického dělení rout, meta helperů, katalogových komponent, test runneru, ESLintu a CI. V aplikaci ale zůstávají systémově důležitá místa:
app/db/herb-detail.server.ts(~560 řádků) — detail byliny skládá hodně nezávislých read modelů.app/db/herbs.server.ts(~430 řádků) — katalog bylin, filter options, list query, slug list a detail fallback jsou stále v jednom modulu.app/components/catalog-filter-form.tsx(~390 řádků) — jeden formulář drží všechny katalogové filtry a UI vazby.- DB moduly (
topics,processing-methods,regions,recipes,spiritual-guides) mají podobné read-only vzory, ale bez společného pojmenování kontraktů. - Smoke test katalogu existuje, ale není obecný vzor pro další veřejné stránky.
V4 proto nemá primárně „zmenšovat soubory“. Má zavést pravidla, podle kterých se budou další změny dělat bezpečně a čitelně.
1. Bezpečnostní rámec pro celý v4
1.1 Klasifikace změn
Každý krok ve v4 musí být před implementací označen jednou třídou:
| Třída | Popis | Povinné ověření |
|---|---|---|
| S0 | Dokumentace, inventura, pojmenování bez kódu | git diff --check |
| S1 | Čistá extrakce bez změny textů, SQL, URL, JSX pořadí | npm run verify |
| S2 | Přesun chování za existujících testů nebo s novými testy před změnou | npm run verify, relevantní test soubor |
| S3 | SQL, URL, public route contract, sitemap, SEO, data seed nebo vizuální layout | testy před změnou, smoke matice, ruční kontrola URL |
Akceptace:
- Každý další task v dokumentu má uvedenou bezpečnostní třídu.
- S3 task se nezačne bez explicitní regresní matice.
1.2 Standardní definition of done
Pro každý dokončený bod:
- Diff je menší než rozumný review balík; pokud není, rozdělit.
- Nejsou změněné veřejné URL, query parametry ani dokumentační slugy, pokud to task výslovně neříká.
-
npm run verifyprošel. - U DB/katalogu prošel relevantní Vitest a při dotyku katalogových filtrů
npm run smoke:byliny-matrix. - Dokumentace tasku je aktualizovaná ve stejném commitu jako změna.
2. Architektonická mapa aplikace
2.1 Popsat vlastnické hranice modulů
Třída: S0
Dnes je struktura praktická, ale hranice nejsou explicitně popsané. Cílem je vytvořit krátkou mapu, která řekne, kam patří nový kód:
app/routes/*— jen route contract:loader,meta, redirect/404, předání dat komponentě.app/components/<domain>/*— prezentační komponenty a drobná UI logika bez D1.app/lib/*— čisté helpery, parsery, meta buildery, URL buildery, labely, DTO transformace bez D1.app/db/*— read modely nad D1, SQL fragmenty, query buildery, žádné JSX.workers/*— edge/provozní wrappery, security headers, media proxy, maintenance.scripts/*— lokální údržba, seed, smoke.
Výstup:
- Doplnit do
docs/tasks/refaktor-v4.mdsekci „Rozhodnutí“ po prvním implementačním kroku, nebo vytvořitdocs/architecture.md, pokud bude mapa růst. - Zapsat pravidlo: route soubor nesmí obsahovat SQL, DB modul nesmí runtime importovat komponentu, komponenta nesmí runtime importovat
app/db.
Rozhodnutí po první várce (2026-05-11):
- Tvrdé pravidlo pro runtime importy:
app/componentsaapp/libnesmí runtime importovatapp/db;app/dbaapp/libnesmí runtime importovatapp/components; vrstvy mimo routes nesmí importovatapp/routes. - Dočasná výjimka: existující
import typez DB modulů do komponent/lib je tolerovaný typový dluh, protože současné komponenty přebírají D1 read model typy přímo. Odstraňovat ho postupně přes úkoly4.1(loader data kontrakty) a3.1(read model split), ne plošně. - Opačný směr
app/lib→app/componentsse nepovoluje ani pro typy. První výskyt byl odstraněn přesapp/lib/topic-link-badge-types.ts.
2.2 Zkontrolovat nežádoucí závislosti
Třída: S1
Bez nového tooling stacku nejdřív ruční kontrola přes rg:
rg -n 'from "../db|from "~/db|from "../../db' app/components app/lib
rg -n 'from "../components|from "~/components|from "../../components' app/db app/lib
rg -n 'from "../routes|from "~/routes|from "../../routes' app/components app/db app/lib
Akceptace:
- Runtime výstupy jsou prázdné. (2026-05-11: kontrolováno přes
rg --pcre2 '^import(?! type).*' …pro směrycomponents/lib -> db,db/lib -> components,components/db/lib -> routes.) - Type-only výskyty jsou zdokumentované jako přechodný dluh. (2026-05-11: komponenty/lib stále importují DB DTO typy; řešit postupně přes loader kontrakty, ne hromadně.)
- Nalezené opačné typové porušení
app/lib→app/componentsbylo přesunuto do čistého lib typu. (TopicHerbLinkBadgePropsje vapp/lib/topic-link-badge-types.ts.)
3. DB/read model vrstva
3.1 Rozdělit herb-detail.server.ts podle read modelů
Třída: S2
app/db/herb-detail.server.ts je největší zbývající soubor. Nejde ho přepsat najednou. Cílem je zavést menší read model moduly po sekcích karty byliny.
Navržené moduly:
app/db/herb-detail-core.server.ts— základ byliny, identifikace, podobné byliny.app/db/herb-detail-harvest.server.ts— harvest periods, storage, occurrence.app/db/herb-detail-processing.server.ts— processing methods, recipes.app/db/herb-detail-science.server.ts— scientific evidence, DOI/zdroje.app/db/herb-detail-safety.server.ts— safety warnings, interactions, contraindications.app/db/herb-detail-spiritual.server.ts— spiritual uses a související vazby.
Pravidla:
- Nejdřív přidat snapshot/regresní Vitest pro transformaci jedné sekce, pokud jde testovat bez D1. (2026-05-11:
app/db/herb-detail-core.server.test.tsprocoreRowToHerb.) - Přesunout vždy jen jednu sekci. (2026-05-11: pouze core row typ + transformace do
app/db/herb-detail-core.server.ts.) - Neměnit SQL text ani bind pořadí ve stejném kroku jako přesun. (SQL dotaz v
getPublishedHerbDetailBundlezůstal beze změny.) -
getPublishedHerbDetailBundlezůstane veřejný entrypoint, dokud není celý split stabilní.
Hotovo — krok 1:
HerbCoreRow,HerbDetailExtendedacoreRowToHerbjsou vapp/db/herb-detail-core.server.ts.app/db/herb-detail.server.tsre-exportujeHerbDetailExtended, aby zůstaly kompatibilní existující type-only importy z komponent.- Cílený test:
npm run test -- app/db/herb-detail-core.server.test.ts.
Zbývá:
- Další sekce řešit po jednom: harvest/storage/occurrence, processing/recipes, science, safety, spiritual.
- U dalších sekcí nejdřív hledat čistou transformaci testovatelnou bez D1; pokud není, začít jen přesunem typu nebo dokumentací hranice.
3.2 Sjednotit read-only DB moduly
Třída: S2
Moduly topics.server.ts, processing-methods.server.ts, regions.server.ts, recipes.server.ts, spiritual-guides.server.ts mají podobný pattern: list, count/detail, slug list. Cílem není generický repository framework, ale konzistentní názvy a návratové typy.
Kandidáti:
- Pojmenovat typy konzistentně:
*ListItem,*Detail,*Summary. - U každého modulu oddělit normalizaci slugů do
app/lib/*-slug.ts, pokud ještě není. - Přidat Vitest pro čistou normalizaci/transformaci, ne pro D1 samotné.
- Nezavádět abstraktní query builder pro všechny DB moduly.
3.3 Katalogové SQL měnit jen přes test-first postup
Třída: S3
V3 už vytvořil buildHerbCatalogFilterIdsQuery, SQL fragmenty a smoke matici. Další zásahy do katalogových filtrů jsou možné pouze test-first.
Před každou změnou:
- Rozšířit
app/db/herb-catalog-filter-ids-query.server.test.tso konkrétní případ. - Rozšířit
docs/tasks/p2-3-katalog-filtry-d1-matice.md, pokud jde o chování viditelné přes HTTP. - Spustit
npm run test -- app/db/herb-catalog-filter-ids-query.server.test.ts. - Po implementaci spustit
npm run verifyanpm run smoke:byliny-matrix.
4. Route contracts a loader data
4.1 Zavést explicitní loader data typy u větších stránek
Třída: S2
U rout po v3 už existují prezentační komponenty, ale loader data kontrakty nejsou všude pojmenované stejně. Cílem je, aby route loader, meta helper a komponenta sdílely jeden čitelný typ.
Kandidáti:
-
HerbDetailLoaderDatapro/byliny/:slug. -
SezonaMonthLoaderDatapro/sezona/:month. -
OverviewRouteLoaderDatajen tam, kde route skutečně má loader data. - Typy držet poblíž route helperu nebo komponenty, ne v globálním
types.ts. (2026-05-11:app/lib/herb-detail-loader-data.ts.)
Akceptace:
- Route soubor zůstane tenký. (
/byliny/:slugjen typuje loader návrat jakoPromise<HerbDetailLoaderData>a skládá existující data.) - Komponenta nepotřebuje znát
Route.ComponentPropsz React Router generovaných typů. (HerbDetailPageProps = HerbDetailLoaderData.) -
npm run verify.
Hotovo — krok 1:
app/lib/herb-detail-loader-data.tsdefinujeHerbDetailLoaderDataaHerbDetailRitualLink.app/routes/byliny.$slug.tsxmá explicitní návratový typ loaderu.app/components/herb-detail/herb-detail-page.tsxpoužívá explicitní page props bezRoute.ComponentProps.app/lib/herb-detail-route-meta.tsodvozuje úzký meta slice zHerbDetailLoaderData, ale nevyžaduje celé loader data v meta testech.
Hotovo — krok 2:
app/lib/sezona-month-loader-data.tsdefinujeSezonaMonthLoaderData.app/routes/sezona.$month.tsxmá explicitní návratový typ loaderu a komponenta používá{ loaderData: SezonaMonthLoaderData }.app/lib/sezona-month-meta.tsodvozuje úzký meta slice zeSezonaMonthLoaderData, ale v testech dál stačí předat jen délkuherbs.
Zbývá:
- Případné další kontrakty jen tam, kde loader data sdílí route, meta a komponenta.
OverviewRouteLoaderDatazatím nepřidávat plošně; nejdřív najít konkrétní přehledovou route, kde by sdílený kontrakt skutečně snížil duplicitu.
4.2 Sjednotit 404 a chybové odpovědi
Třída: S2
Dnes routy vytvářejí throw new Response(...) lokálně. To je funkční, ale každá route řeší text/status samostatně.
Směr:
- Přidat malý helper typu
notFoundResponse(message?: string)doapp/lib/route-responses.ts. - Přesunout nejdřív jednu route, například
dokumentace.$slug.tsx, kde je chování jednoduché. - Neměnit status kódy ani text, pokud už text existuje. (2026-05-11:
dokumentace.$slug.tsxzůstává 404 s prázdným body.)
Hotovo — krok 1:
app/lib/route-responses.tsdefinujenotFoundResponse(message?: string): Response.app/lib/route-responses.test.tsověřuje výchozí prázdné body i volitelnou zprávu.app/routes/dokumentace.$slug.tsxpoužívá helper pro oba existující 404 případy bez změny statusu nebo textu.
Zbývá:
- Případné další chybové helpery přidávat jen po konkrétní duplicitě; zatím nepřevádět
roadmap.tsx, protože jde o 500 s konkrétním textem.
Pozor: neřešit zatím globální error boundary design; to je produktový/UX task.
5. UI systém bez vizuálního přepisu
5.1 Rozdělit catalog-filter-form.tsx interně
Třída: S2 až S3 podle rozsahu
CatalogFilterForm je největší frontend soubor. Cílem není změnit layout, ale rozdělit opakující se skupiny polí.
Navržené malé komponenty:
CatalogSearchFieldCatalogMonthRegionFieldsCatalogProcessingFieldsCatalogScienceFieldsCatalogSafetyPartFieldsCatalogSpiritualField
Bezpečnostní pravidla:
- Nejprve extrahovat jednu skupinu polí. (2026-05-11: pouze horní vyhledávací řádek jako lokální
CatalogSearchField.) - Beze změny
name,defaultValue,defaultChecked,key,disabled,aria-*. (U search fieldu zůstalyid,key,name,type,defaultValue, placeholder i třídy stejné; checkboxy/selecty se neměnily.) - Ruční kontrola
/bylinys prázdnou URL i s aktivními filtry. (2026-05-11:npm run smoke:byliny-matrixprošel R1–R15; preview kontrola/bylinya aktivní URL potvrdila renderid="herb-search",name="q"a hodnotuq=kopriva.) - Screenshot nebo detailní poznámka před/po. (Detailní poznámka: jde o čistý přesun stejného JSX do privátní komponenty ve stejném souboru, bez změny pořadí DOM v rámci
<Form>.)
Hotovo — krok 1:
app/components/catalog-filter-form.tsxobsahuje privátníCatalogSearchField.- Veřejné props
CatalogFilterFormPropsse nezměnily. - Extrakce se nedotkla strukturovaných filtrů, URL parametrů ani DB dotazů.
- Ověření:
npm run verify,npm run smoke:byliny-matrix, HTTP kontrola/bylinya aktivní filtrovací URL přes preview.
Zbývá:
- Další skupiny extrahovat po jedné; jako další kandidát dává smysl
CatalogMonthRegionFields, ale až po samostatném diffu.
5.2 Zavést malé UI primitivy jen tam, kde už je opakování
Třída: S2
Nezavádět design system „shora“. Přidat primitivum jen když odstraní konkrétní duplicitu.
Kandidáti:
-
SectionEyebrowneboPageIntro, pokud se opakující nadpisové bloky začnou měnit na více místech. -
ChipLinkpouze pokud sjednotí existující chip linky bez změny tříd. -
EmptyStatepouze tam, kde jsou shodné prázdné stavy s rozdílným textem.
Co nedělat:
- Nevytvářet univerzální
Cardkomponentu pro celý web. - Nepřepisovat Tailwind třídy plošně.
- Neměnit vizuální rytmus stránek bez samostatného UI tasku.
6. Testovací strategie v4
6.1 Testovací pyramidy podle typu změny
Třída: S0/S1 dokumentační základ, S2 při přidávání testů
| Oblast | Preferovaný test |
|---|---|
| Čisté parsery/helpery | Vitest unit test |
| Meta helpery | Vitest nad vráceným polem metadat |
| URL buildery | Vitest nad přesným query stringem |
| SQL buildery | Vitest nad SQL/binds + D1 smoke matice |
| Route loadery | nejdřív extrakce čisté části; loader testovat až po zavedení stabilního harnessu |
| UI bez změny chování | screenshot/ruční URL kontrola, případně až později e2e runner |
6.2 Sdílená fixture data pro helper testy
Třída: S2
Jak testů přibývá, začnou se kopírovat stejné slugy/měsíce/labely. Přidat fixture jen tehdy, když je duplicita reálná.
Kandidáti:
-
app/test/fixtures/catalog.ts -
app/test/fixtures/herb-detail.ts -
app/test/assert-meta.ts
Pravidla:
- Fixture nesmí importovat D1 ani route moduly.
- Fixture nesmí být větší než test, který nahrazuje, pokud se používá jen jednou.
7. Provoz, smoke a CI
7.1 Rozšířit smoke scénáře mimo katalog
Třída: S2/S3
smoke:byliny-matrix chrání katalogové filtry. V4 může přidat podobné smoke scénáře pro veřejné stránky, ale jen po stabilizaci potřeby.
Kandidáti:
-
/sezona/:monthzákladní měsíc +?region=. -
/byliny/:slugznámá bylina s obrázky, vědou, bezpečností a zpracováním. -
/dokumentace/:slugznámý dokument. -
/sitemap.xmlobsahuje kritické route skupiny.
Akceptace:
- Smoke script běží lokálně bez remote služeb.
- CI ho nespouští automaticky, dokud není stabilní runtime a čas.
7.2 Worker observabilita bez nového stacku
Třída: S2
Worker už má strukturovaný maintenance log. Další krok je konzistence, ne nový monitoring.
Kandidáti:
- Jednotný helper pro JSON log eventy ve
workers/. - Zachovat jednoduché
console.log/console.error, ale sjednotit polelevel,component,event,ok. - Nepřidávat externí observability službu v rámci refaktoru.
8. Dokumentace a proces
8.1 Stav refaktor dokumentů
Třída: S0
- Po založení v4 doplnit
docs/tasks/README.md. - Po dokončení bezpečného rozsahu přesunout starší hotové refaktory do
docs/tasks/done/. - V každém refaktor dokumentu udržet jasný stav:
Návrh,Částečně,Hotovo,Archiv.
8.2 Architektonická rozhodnutí
Třída: S0
Pokud se v4 začne opakovaně vracet k otázkám typu „kam patří loader DTO“ nebo „kdy se smí měnit SQL“, založit lehký ADR styl bez tooling:
-
docs/architecture-decisions.md - Každé rozhodnutí: datum, kontext, rozhodnutí, důsledky.
- Nepřidávat ADR pro jednorázové drobné helpery.
9. Doporučené pořadí implementace
- S0/S1: doplnit v4 do
docs/tasks/README.mda udělat ruční dependency boundary audit. - S2: vybrat jednu čistou část z
herb-detail.server.ts, napsat/rozšířit test transformace a přesunout ji do read model modulu. - S2: pojmenovat loader data kontrakt pro jednu velkou route, nejspíš
/byliny/:slug. - S2/S3: extrahovat jednu skupinu polí z
CatalogFilterFormbez změny atributů a URL. - S2: přidat první obecnější smoke scénář mimo katalog, až bude jasné, která URL je nejkritičtější.
- S0: vyhodnotit, jestli vznikla potřeba
docs/architecture-decisions.md.
10. Co zatím nedělat v rámci v4
- Nepřepisovat celý DB layer na generický repository framework.
- Nezavádět ORM.
- Nepřepisovat route strom ani veřejné URL.
- Neměnit sitemap/canonical pravidla bez SEO tasku.
- Nepřidávat Playwright nebo jiný e2e runner ve stejném kroku jako UI refaktor.
- Nepřepisovat
CatalogFilterFormnajednou. - Nezavádět globální design system, dokud není jasná opakovaná duplicita.
Akceptace dokumentu
- Plán navazuje na reálný stav po v3.
- Každý implementační směr má bezpečnostní třídu nebo pravidla pro její určení.
- Dokument rozlišuje bezpečné kroky, střední riziko a S3 změny.
- Dokument obsahuje doporučené pořadí a explicitní seznam věcí, které zatím nedělat.