Přeskočit na obsah

Refaktor v4 — systémová architektura, hranice a dlouhodobá udržitelnost

Stav dokumentu: Částečně — první S0/S1 várka hotová; 3.1 má první S2 řez (herb-detail-core.server.ts + Vitest) bez změny SQL; 4.1 má loader kontrakty pro /byliny/:slug a /sezona/:month; 4.2 má první 404 helper řez pro dokumentační detail; 5.1 má 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řídaPopisPovinné ověření
S0Dokumentace, inventura, pojmenování bez kódugit diff --check
S1Čistá extrakce bez změny textů, SQL, URL, JSX pořadínpm run verify
S2Přesun chování za existujících testů nebo s novými testy před změnounpm run verify, relevantní test soubor
S3SQL, URL, public route contract, sitemap, SEO, data seed nebo vizuální layouttesty 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 verify proš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.md sekci „Rozhodnutí“ po prvním implementačním kroku, nebo vytvořit docs/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/components a app/lib nesmí runtime importovat app/db; app/db a app/lib nesmí runtime importovat app/components; vrstvy mimo routes nesmí importovat app/routes.
  • Dočasná výjimka: existující import type z 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 úkoly 4.1 (loader data kontrakty) a 3.1 (read model split), ne plošně.
  • Opačný směr app/libapp/components se nepovoluje ani pro typy. První výskyt byl odstraněn přes app/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ěry components/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/libapp/components bylo přesunuto do čistého lib typu. (TopicHerbLinkBadgeProps je v app/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.ts pro coreRowToHerb.)
  • 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 getPublishedHerbDetailBundle zůstal beze změny.)
  • getPublishedHerbDetailBundle zůstane veřejný entrypoint, dokud není celý split stabilní.

Hotovo — krok 1:

  • HerbCoreRow, HerbDetailExtended a coreRowToHerb jsou v app/db/herb-detail-core.server.ts.
  • app/db/herb-detail.server.ts re-exportuje HerbDetailExtended, 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.ts o 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 verify a npm 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:

  • HerbDetailLoaderData pro /byliny/:slug.
  • SezonaMonthLoaderData pro /sezona/:month.
  • OverviewRouteLoaderData jen 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/:slug jen typuje loader návrat jako Promise<HerbDetailLoaderData> a skládá existující data.)
  • Komponenta nepotřebuje znát Route.ComponentProps z React Router generovaných typů. (HerbDetailPageProps = HerbDetailLoaderData.)
  • npm run verify.

Hotovo — krok 1:

  • app/lib/herb-detail-loader-data.ts definuje HerbDetailLoaderData a HerbDetailRitualLink.
  • app/routes/byliny.$slug.tsx má explicitní návratový typ loaderu.
  • app/components/herb-detail/herb-detail-page.tsx používá explicitní page props bez Route.ComponentProps.
  • app/lib/herb-detail-route-meta.ts odvozuje úzký meta slice z HerbDetailLoaderData, ale nevyžaduje celé loader data v meta testech.

Hotovo — krok 2:

  • app/lib/sezona-month-loader-data.ts definuje SezonaMonthLoaderData.
  • app/routes/sezona.$month.tsx má explicitní návratový typ loaderu a komponenta používá { loaderData: SezonaMonthLoaderData }.
  • app/lib/sezona-month-meta.ts odvozuje úzký meta slice ze SezonaMonthLoaderData, ale v testech dál stačí předat jen délku herbs.

Zbývá:

  • Případné další kontrakty jen tam, kde loader data sdílí route, meta a komponenta.
  • OverviewRouteLoaderData zatí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) do app/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.tsx zůstává 404 s prázdným body.)

Hotovo — krok 1:

  • app/lib/route-responses.ts definuje notFoundResponse(message?: string): Response.
  • app/lib/route-responses.test.ts ověřuje výchozí prázdné body i volitelnou zprávu.
  • app/routes/dokumentace.$slug.tsx použí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:

  • CatalogSearchField
  • CatalogMonthRegionFields
  • CatalogProcessingFields
  • CatalogScienceFields
  • CatalogSafetyPartFields
  • CatalogSpiritualField

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ůstaly id, key, name, type, defaultValue, placeholder i třídy stejné; checkboxy/selecty se neměnily.)
  • Ruční kontrola /byliny s prázdnou URL i s aktivními filtry. (2026-05-11: npm run smoke:byliny-matrix prošel R1–R15; preview kontrola /byliny a aktivní URL potvrdila render id="herb-search", name="q" a hodnotu q=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.tsx obsahuje privátní CatalogSearchField.
  • Veřejné props CatalogFilterFormProps se 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 /byliny a 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:

  • SectionEyebrow nebo PageIntro, pokud se opakující nadpisové bloky začnou měnit na více místech.
  • ChipLink pouze pokud sjednotí existující chip linky bez změny tříd.
  • EmptyState pouze tam, kde jsou shodné prázdné stavy s rozdílným textem.

Co nedělat:

  • Nevytvářet univerzální Card komponentu 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ů

OblastPreferovaný test
Čisté parsery/helperyVitest unit test
Meta helperyVitest nad vráceným polem metadat
URL builderyVitest nad přesným query stringem
SQL builderyVitest nad SQL/binds + D1 smoke matice
Route loaderynejdří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/:month základní měsíc + ?region=.
  • /byliny/:slug známá bylina s obrázky, vědou, bezpečností a zpracováním.
  • /dokumentace/:slug známý dokument.
  • /sitemap.xml obsahuje 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 pole level, 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

  1. S0/S1: doplnit v4 do docs/tasks/README.md a udělat ruční dependency boundary audit.
  2. S2: vybrat jednu čistou část z herb-detail.server.ts, napsat/rozšířit test transformace a přesunout ji do read model modulu.
  3. S2: pojmenovat loader data kontrakt pro jednu velkou route, nejspíš /byliny/:slug.
  4. S2/S3: extrahovat jednu skupinu polí z CatalogFilterForm bez změny atributů a URL.
  5. S2: přidat první obecnější smoke scénář mimo katalog, až bude jasné, která URL je nejkritičtější.
  6. 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 CatalogFilterForm najednou.
  • 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.