Skip to content

Latest commit

 

History

History
1053 lines (1019 loc) · 193 KB

File metadata and controls

1053 lines (1019 loc) · 193 KB

Podrobná Vývojová Dokumentace

Tento dokument slouží jako detailní technická dokumentace vývoje. Pro centralizovaný přehled hlavních route handler kontraktů používej i docs/API.md; implementace v src/app/api/**/route.ts je ale vždy finální zdroj pravdy.

Kořenová technická dokumentace

  • Architektonický přehled repa je v ARCHITECTURE.md.
  • End-to-end veřejný booking a self-service tok je v BOOKING_FLOW.md.
  • Stručný provozní deployment přehled pro Proxmox/LXC je v DEPLOYMENT.md.
  • Stručný runtime přehled proměnných a prostředí je v ENVIRONMENT.md.
  • Pro opakující se incidenty použij i TROUBLESHOOTING.md.
  • Aktuální pravidla automatického oběda, jeho denních override, planner read modelu a authoritative booking ochrany jsou shrnutá v SCHEDULE_OPTIMIZATION_MIGRATION.md; historické fáze v tomto dokumentu nepřepisují jeho úvodní výsledný stav.

Verzování a release disciplína

  • package.json používá SemVer MAJOR.MINOR.PATCH; aktuální release ověř vždy přímo v package.json.
  • Praktické pravidlo pro tento projekt:
    • PATCH: bugfix, interní refaktor bez změny kontraktu, performance tuning bez změny chování API/UI kontraktu.
    • MINOR: nová funkce nebo rozšíření existující funkce zpětně kompatibilním způsobem.
    • MAJOR: nekompatibilní změna (API, route contract, data contract, provozní workflow vyžadující změnu postupu).
  • Průběžná práce: každá významná změna produkčního chování doplní ve stejné změně uživatelsky srozumitelný bod do CHANGELOG.md / Unreleased; verzi v package.json neměň a historické verzované sekce neupravuj. Pravidla a výjimky stanoví ADR 0069 a AGENTS.md.
  • Před odevzdáním produkční změny spusť npm run changelog:check -- --base <základní-commit-nebo-větev>. Výjimka v PR vyžaduje label skip-changelog a řádek Důvod pro skip-changelog: <konkrétní důvod> v popisu; je určená jen pro prokazatelně nebehaviorální změny.
  • Příprava releasu: zvol novou SemVer verzi, přesuň obsah Unreleased pod novou verzovanou sekci s datem, aktualizuj případné odkazy pro porovnání verzí, vytvoř novou prázdnou Unreleased a atomicky sjednoť verzi v package.json, kořenovém package-lock.json a dalších skutečně používaných místech. Před commitem spusť npm run version:check; ověřuje manifesty i všechny dokumenty označené <!-- current-version -->. Release commit obsahuje finální version bump i odpovídající release poznámky.
  • Pro standardní produkční rollout používej ./deploy/release.sh; skript vytváří celý release v releases/<commit>-<čas> a atomicky přepíná current. Pořadí je pull -> staging npm ci -> generate -> kontrola historie -> prisma validate -> lint -> typecheck -> test:release -> build bez DB čtení aplikace -> stop webu a workeru -> ověření, že writeři neběží -> migrate deploy -> voucher:templates:bootstrap z nového release -> přepnutí current -> start webu i workeru -> health + smoke. Bootstrap je povinný a idempotentní, běží se stejným env/storage/dependencies jako nový runtime; jeho selhání je fail-closed. test:release nespouští DB integrační scénáře nad produkčním .env; ty zůstávají povinnou CI bránou. Po migraci je rollback fail-closed: symlink se může vrátit, ale starý runtime se nad změněným schématem automaticky nespouští.
  • Po potvrzeném releasu helper provede retenci verzovaných adresářů: chrání current a previous; další releasy ve výchozím nastavení neuchovává. Počet dalších nejnovějších release lze nastavit přes --keep-releases. Úklid nesmí běžet před health/smoke testem ani mazat jinak pojmenované provozní adresáře.
  • Release kontrola používá bez-DB /api/health/live při čekání na listener, poté readiness /api/health a samostatnou homepage smoke URL. Při změně skriptu zachovej rozlišené provozní logy včetně HTTP statusu, aby se 500 z homepage nezaměnila za health endpoint.
  • Po systemctl start nejdřív použij tichý probe webového endpointu; Next.js může několik stovek milisekund až jednotek sekund otevírat port. Teprve po otevření portu spouštěj viditelný health/smoke test.
  • Na npm 11 držíme v package.json i allowScripts whitelist pro balíčky s install hooky (prisma, @prisma/engines, sharp, esbuild, unrs-resolver). Při upgradu některého z nich čekej změnu pinu a po review spusť znovu npm approve-scripts <pkg>, jinak budou releasy hlásit npm warn allow-scripts.

Dev runtime a cache

Voucher Template Manager

  • VoucherTemplate je verzovaný immutable master: draft lze upravit, PUBLISHED a INACTIVE se nikdy nepřepisují. Nový design vzniká klonem do nové verze.
  • Master PDF je privátní soubor v MEDIA_STORAGE_ROOT/private/voucher-templates; databáze drží pouze relativní cestu a SHA-256. Pro first rollout spusť explicitně npm run voucher:templates:bootstrap po migraci, nikdy při web requestu. Čistý rollout vytvoří historický neaktivní classic-v1 pro reprint/backfill starých voucherů a publikovaný strict classic-v2 pro nové vydání a tisk.
  • Voucher.templateId, VoucherPrintBatch.templateId a SiteSettings.voucherDefaultTemplateId jsou referenční vazby. Default musí odkazovat na publikovanou šablonu; před deaktivací jej změň.
  • PRINT (216 × 105 mm), DIGITAL (vektorový crop 210 × 99 mm) i STOCK používají stejný persistentní master. Tiskový preflight strukturálně kontroluje PDF header minimálně 1.6, geometrii, OutputIntent, ICC header vhodný pro CMYK tisk (N=4, acsp, class, color space, PCS), přesné katalogové PDF/X-4 XMP metadata (GTS_PDFXVersion=PDF/X-4 bez GTS_PDFXConformance), rotaci, encryption a všechny stránky; bez externího PDF/X validátoru reportuje STRUCTURALLY_VALIDATED, nikoli externí certifikaci.

Vývoj e-mailů

Produkční HTML templates jsou v src/lib/email/react-email/ a sdílené komponenty v _components. npm run email:dev slouží k interaktivnímu vývoji React Email šablon, zatímco npm run email:previews vytváří regresní preview přes aplikační renderer. renderEmailTemplate(...) zůstává orchestration hranicí; React Email e-maily negeneruje ani neposílá, transport pokračuje přes současnou delivery infrastrukturu. Plain-text generování a přílohy zůstávají v orchestrace.

  • Vývoj i CI standardizujeme na Node 24 LTS. Repo drží .nvmrc s hodnotou 24 a package.json deklaruje engines.node = ^24.0.0; před prvním npm install nebo po upgrade runtime si ověř node -v.
  • Výchozí npm run dev používá Next.js 16 dev server s Turbopackem kvůli rychlosti kompilace velké admin route. Webpack je dostupný přes npm run dev:webpack jako stabilnější fallback při problémech s Turbopack HMR.
  • next.config.ts nastavuje turbopack.root na __dirname; při verzovaném release/staging buildu tak Turbopack nepovažuje sourozenecké releases/*/package-lock.json za další workspace. Při přesunu projektu neměň tuto hodnotu na pevnou absolutní cestu.
  • U admin formulářů nepočítej s tím, že stylování nativního <select>/<option> bude konzistentní napříč OS. Chrome na Windows může ignorovat kontrast očekávaný z Tailwind tříd a vykreslit nečitelný dropdown; pro business-kritický výběr (např. voucher na službu) preferuj vlastní seznam/radio/button picker nad hidden inputem.
  • U klientských admin formulářů na Next.js 16 nepoužívej useCallback jen kvůli lokálním helperům typu resetForm(), pokud se nikam nepředávají jako props. Když stejný helper potřebuješ volat z useEffect i z click handlerů, preferuj v React 19 useEffectEvent; lint react-hooks/preserve-manual-memoization i exhaustive-deps tak zůstane v souladu s React compilerem.
  • Pokud dev server spadne na poškozené Turbopack cache (Failed to restore task data, chybějící .sst v .next/dev/cache/turbopack), použij:
    • npm run dev:clean (zkontroluje historii a aktuálnost DB migrací, smaže .next a znovu spustí dev server)
    • npm run dev:webpack (explicitní Webpack režim; stabilnější, ale pomalejší kompilace)
  • Pokud browser v dev režimu po restartu nebo invalidaci layoutu skončí na ChunkLoadError: Failed to load chunk /_next/static/chunks/..., root shell teď provede automatický hard reload už z inline beforeInteractive guardu v root layoutu, takže ochrana funguje i když spadne samotný klientský layout chunk. Guard používá krátké 15s okno se dvěma pokusy a dočasný query parametr __ppstudio_chunk_reload, který po úspěšném načtení sám odstraní; tím se nezasekne na trvalém sessionStorage locku po prvním nepovedeném refreshi. Když ani potom načtení nepomůže, vyčisti .next přes npm run dev:clean; nejde typicky o business chybu /rezervace, ale o rozjetý HMR/client cache stav.
  • Uploady souborů přes Server Actions respektují Next.js request body limit. Pro admin Média je v next.config.ts nastaveno experimental.serverActions.bodySizeLimit = "10mb", protože samotný business limit obrázku je 8 MB a multipart formulář přidává overhead navíc.
  • Kořenový instrumentation.ts je oficiální Next.js 16 hook pro serverovou observability. Když potřebuješ víc detailů k chybám, které spadnou ještě před vlastní action logikou, přidávej je přes register() / onRequestError, ne až do jednotlivých server actions.

Test runner a coverage

Prague local time

resolvePragueLocalDateTime nejdříve ověřuje rozsahy data a času a následně vyžaduje přesný round-trip přes Europe/Prague. Časy v jarní DST mezeře a neplatná data se odmítají (null). Dvojznačný čas při podzimním přechodu používá dřívější výskyt.

  • npm run test:unit používá Node test runner + tsx preload nad quoted globem src/**/*.test.{ts,tsx}; quoting je záměrný, protože bez něj Bash v defaultní konfiguraci expandoval jen část stromu a coverage pak nereprezentovala celé repo. Unit soubory běží v izolovaných procesech s výchozí concurrency min(4, dostupná paralelní jádra); pro diagnostické sériové spuštění použij TEST_CONCURRENCY=1. npm test zachovává plný lokální preflight: spustí nejdřív unit vrstvu a potom DB integrace.
  • npm run typecheck drží rychlou čistou TypeScript vrstvu přes tsc --noEmit; je to záměrně samostatný check, aby typové regrese nebyly vidět až v next build.
  • npm run test:coverage používá c8 nad tím samým runnerem a ukládá výstupy do coverage/.
  • U TypeScript callbacků vracejících union stavů planneru (available / locked / booked / inactive) preferuj u flatMap explicitní generic typu flatMap<PlannerInterval>(...). Next.js 16 build checker jinak umí vnořené větve zúžit jen podle první literal větve a shodit production build na neplatné kompatibilitě status.
  • U admin planneru rozlišuj dvě vrstvy chování: editace stále pracuje s 30min sloty/cells, ale read model pro UI posílá i cleanupBlocks / availableBlocks v minutách, aby šlo jednu půlhodinovou buňku vykreslit po 15 minutách.
  • availableIntervals v planner read modelu neskládej přímo z jednotlivých slotových intervalů. Nejprve mergeuj availableBlocks v minutách a teprve potom je převáděj na celé 30min editable úseky, jinak se rozpadnou legitimně navazující sloty typu 14:00–14:45 + 14:45–15:00.
  • Coverage scope je záměrně business-first:
    • src/features/booking/lib/**/*.ts
    • src/features/admin/lib/**/*.ts
    • src/features/admin/actions/**/*.ts
    • src/features/vouchers/lib/**/*.ts
    • src/lib/email/**/*.ts
  • Report generuje formáty text-summary, html, lcov a json-summary, takže se hodí jak pro lokální čtení, tak pro CI artefakty.
  • Coverage běh nezapíná RUN_DB_INTEGRATION_TESTS=1; díky tomu měří hlavně unit/business logiku a nespadne na prostředí bez lokální databáze. npm run test:db:integration spouští aktuálně 27 DB integračních souborů samostatně proti připravenému PostgreSQL a vždy sériově přes --test-concurrency=1; glob automaticky zahrne i další budoucí soubory. npm run test:ci tuto vrstvu povinně navazuje za coverage, takže ji CI nemůže omylem přeskočit.
  • GitHub Actions baseline je teď rozdělená do čtyř vrstev:
    • hlavní CI jako osm samostatných jobů: lint, typecheck, test, build, e2e, e2e chromium shard 2, e2e mobile a e2e mobile shard 2; coverage je krok a artefakt jobu test, nikoli samostatný check
    • Dependency Review pro PR dependency diff
    • CodeQL pro statickou security analýzu javascript-typescript
    • Security Audit pro scheduled/push npm audit --audit-level=high
  • Verze GitHub Actions drž vědomě blízko aktuálním major release, protože starší akce mohou na GitHub runneru skončit v compat režimu s Node deprecation warningy. Aktuální baseline v repu je actions/checkout@v7, actions/setup-node@v7, actions/upload-artifact@v7 a actions/dependency-review-action@v5.
  • Hlavní CI po doběhu ukládá artifacty coverage/ a playwright-report/; při ladění flake nebo regressí tak preferuj stažení artifactu z GitHubu před slepým lokálním rerunem.
  • .github/dependabot.yml drží týdenní update PR pro npm i github-actions; pokud se změní cadence releasů nebo údržbové kapacity týmu, aktualizuj tento soubor spolu s docs/DEPENDENCIES.md.
  • Pro rychlé navyšování coverage v admin vrstvě používej samostatné *.test.ts i pro akční state moduly (src/features/admin/actions/*action-state.ts), protože i tyto server action kontrakty jsou součástí veřejného chování UI formulářů.
  • U server actions v src/features/admin/actions/*.ts prioritně pokrývej validační early-return větve (invalid form payload) bez DB přístupu; je to stabilní low-flake vrstva, která rychle zavírá velké coverage mezery.
  • Stejný postup používej i pro service-category-actions: validační větve create/update vrací strukturovaný action state ještě před auth/DB, takže jsou vhodné pro rychlé unit testy s vysokým poměrem přínos/údržba.
  • Když refaktoruješ velkou klientskou komponentu typu admin planner nebo detail rezervace, vytáhni nejdřív pure helpery do samostatného souboru a přidej jim úzké Node testy. Je to bezpečnější než rovnou rozbíjet komponentu do mnoha nových child komponent bez testovatelného středu.
  • Pro booking-public/engine.ts drž minimálně unit coverage early-fail větví bez DB závislostí (invalid startsAt, invalid phone), aby základní validační guardy nešly regresí obejít.
  • DB integrační testy nespouštěj proti fixním hodinám typu „za 3 dny v 09:00“, pokud běží nad sdílenou nebo již seedovanou databází. Helpery pro sloty mají nejdřív najít izolované budoucí okno bez překryvu v AvailabilitySlot i aktivních Booking, jinak budou flaky podle aktuálních dat nebo paralelních běhů.
  • Stejné pravidlo platí i pro reschedule integrační testy: cílový původní i nový termín musí být před seedem ověřený proti existujícím slotům a aktivním rezervacím, jinak test může falešně skončit chybou Nový termín koliduje s jinou aktivní rezervací. i bez regresní změny doménové logiky.
  • Pro ruční admin booking teď držíme dvě oddělené regresní jistoty: booking-local-time.test.ts hlídá Prague wall-clock převod a booking-manual.integration.test.ts hlídá, že slot režim už nesmí tiše fallbacknout do manualOverride, zatímco explicitní manual režim to stále smí udělat.

Lokální setup workflow

  • Výchozí pořadí pro nový stroj nebo čistý checkout je:
    1. cp .env.example .env
    2. doplnit lokální DATABASE_URL, SHADOW_DATABASE_URL, ADMIN_SESSION_SECRET, NEXT_PUBLIC_APP_URL
    3. nvm use (nebo jiný ekvivalentní přepínač na Node 24 podle .nvmrc)
    4. npm install
    5. npm run db:generate
    6. npm run db:migrate
    7. npm run dev
  • Pro lokální vývoj preferuj EMAIL_DELIVERY_MODE=log; reálné SMTP a veřejný Matomo tracking zapínej jen při cíleném integračním testu.
  • Clarity ve vývoji zapínej jen cíleně (NEXT_PUBLIC_CLARITY_ENABLED=true + NEXT_PUBLIC_CLARITY_PROJECT_ID), default má zůstat vypnutý.
  • Google Ads tag ve vývoji zapínej jen cíleně (NEXT_PUBLIC_GOOGLE_ADS_ENABLED=true + NEXT_PUBLIC_GOOGLE_ADS_ID=AW-...), default má zůstat vypnutý.
  • Meta Pixel ve vývoji zapínej jen cíleně (NEXT_PUBLIC_META_PIXEL_ENABLED=true + NEXT_PUBLIC_META_PIXEL_ID), default má zůstat vypnutý.
  • Recovery admin přístupu prováděj pouze offline příkazem npm run admin:recover-owner -- --email owner@example.com --name 'Jméno' --confirm < heslo.txt; webový bootstrap login neexistuje.
  • README na GitHubu má fungovat jako rozcestník i rychlý onboarding. Když měníš setup, deploy nebo monitoring workflow, promítni změnu do README.md a udržuj v něm krokový postup, ne jen seznam odkazů.

Voucher Stock v1

Předtištěné voucherové série používají modely VoucherPrintBatch, VoucherStockItem a VoucherStockAuditLog. VoucherStockItem.code je rezervovaný globálně proti Voucher.code; alokace kódu i čísla série probíhá uvnitř serializované PostgreSQL transakce s transaction advisory lockem. Běžné vystavení voucheru proto nesmí generovat kód mimo transakci, která následně zapisuje Voucher.

Tisková série vzniká atomicky v routě /admin/vouchery/predtistene, její PDF je deterministický výstup existujících kusů a každý kus má jednu stránku se stejným masterem, kódem a QR. Batch PDF lze stáhnout opakovaně pouze ve stavu PENDING_PRINT; po přechodu do RECEIVED nebo CLOSED route download odmítne a další fyzický tisk vyžaduje novou sérii. Stavový workflow batch je PENDING_PRINT → RECEIVED → CLOSED, workflow kusu PENDING_PRINT → AVAILABLE → ACTIVATED|VOID; příjem, aktivace, VOID a uzavření série jsou auditované a aktivace je idempotentní podle skladového kusu/kódu. SALON nemůže sérii založit, stáhnout ani uzavřít, ale může přijmout aktivaci a označit dostupný kus jako VOID.

Při změně schématu dodrž standardní Prisma workflow: před migrací npx prisma migrate status, potom npx prisma migrate dev --name <název>, a po migraci npx prisma validate, npx prisma generate a npx prisma migrate status. DB scénář Voucher Stock se spouští izolovaně přes RUN_DB_INTEGRATION_TESTS=1 a je určen pro DEV databázi.

Architektura

  • Při změně mobilního admin UI drž minimální výšku hlavních dotykových ovladačů alespoň 2.75rem a nenechávej dlouhou řadu filtrů zalomit se do nečitelných řádků: na telefonu může být vodorovně posuvná, na širším breakpointu se vrací běžné zalomení. U týdenního planneru musí fixed sheet i publish lišta počítat s env(safe-area-inset-bottom).
  • src/app obsahuje pouze routy, layouty a route handlers.
  • src/components drží čistě sdílené stavební prvky.
  • src/features seskupuje konkrétní produktové oblasti:
    • home
    • public
    • booking
    • admin
    • vouchers
  • src/lib obsahuje infrastructure kód bez prezentační logiky.
  • src/lib/media drží infrastrukturní vrstvu pro lokální ukládání a čtení médií.
  • src/config drží metadata, navigaci a validované prostředí.
  • src/content drží editovatelná data veřejného webu odděleně od layoutu a route souborů.
  • Fallback texty ve src/content/public-site.ts musí působit jako finální veřejný obsah (bez interních výrazů typu placeholder/TODO), aby i při výpadku DB copy zůstal web důvěryhodný a produkčně použitelný.
  • U větších admin modulů nedrž zároveň route orchestration, server fetch logiku, pure business helpery a JSX v jednom souboru. Aktuální baseline je:
    • src/features/admin/lib/admin-route-factories.tsx jen pro routing a guardy
    • src/features/admin/lib/admin-settings-page-data.ts pro server-only read model nastavení
    • src/features/admin/components/*-helpers.ts pro čistou synchronní logiku testovatelnou bez React renderu
    • src/features/admin/components/*.tsx pro samotnou kompozici UI

Route Strategie

  • (public) pro prezentační web.
  • (booking) pro rezervace bez míchání admin logiky.
  • (admin) pro backoffice.
  • Booking-specifické globální selektory (např. landscape tweak pro booking header/sticky CTA) drž v src/app/(booking) route-level CSS importovaném v booking layoutu, ne v root src/app/globals.css, aby se tyto styly nenačítaly na homepage.
  • Další vnitřní route group (protected) uvnitř adminu chrání sekce vyžadující session.
  • Veřejné booking flow používá server-loaded page + klientský wizard + server action pro finální zápis.
  • U všech veřejných i admin formulářů nad Next.js Server Actions počítej s deploy skew: aplikace čte deploymentId z NEXT_DEPLOYMENT_ID nebo fallbacku DEPLOYMENT_VERSION / GIT_HASH, ale plná ochrana funguje jen když všechny produkční instance stejného buildu sdílí i stejný NEXT_SERVER_ACTIONS_ENCRYPTION_KEY.
  • deploy/release.sh zapisuje release identifikátory nejen do build env, ale i do .release-env pro systemd runtime. Pokud měníš release flow nebo service unitu, zachovej, že next build, next start a instrumentation.ts vidí stejný deployment identifikátor; jinak se ppstudio.next.register a ppstudio.next.request-error rozjedou s realitou.
  • instrumentation.ts:onRequestError už pro Failed to find Server Action loguje sanitizované request headers (x-deployment-id, proxy/IP metadata, user-agent), route context (routeType: action), fingerprint NEXT_SERVER_ACTIONS_ENCRYPTION_KEY a bezpečné shrnutí next-action headeru. Při dalších úpravách zachovej zásadu, že do těchto logů nesmí spadnout raw booking tokeny, celé query stringy ani plné interní action ID.
  • Ve veřejném booking flow platí focus pravidlo: klik na den v kalendáři má převést fokus na sekci Dostupné časy, zatímco klik na konkrétní čas má převést fokus na první input kontaktního kroku. Při dalších UX úpravách tenhle sled zachovej, aby zůstal konzistentní pro klávesnici i mobilní scroll.
  • Dny v booking kalendáři bez jediného reálně volného času (all slots disabled) mají zůstat neinteraktivní (disabled) a aria-label dnů má používat lidský formát přes formatDateKeyLabel(...), ne surové YYYY-MM-DD.
  • Error a empty stavy veřejného booking flow musí být akční: u chybějících služeb/termínů nebo konfliktů nabídni konkrétní další krok (/kontakt, /cenik nebo návrat do příslušného kroku) a drž klidný prémiový tón PP Studia ve Zlíně.
  • U useActionState flow na veřejných tokenových stránkách nerevaliduj právě otevřenou route jen kvůli cache invalidaci. V Next.js 16 to po server action může vyvolat route refresh a resetovat lokální success/error stav klientské komponenty dřív, než se vykreslí.
  • U veřejných analytics trackerů, které kombinují inline next/script bootstrap a navazující App Router useEffect, drž sdílený runtime marker o tom, že už byla bezpečná route zapsaná do fronty. Bez něj jsou tokenové self-service routy náchylné na race mezi hydratací, Suspense a E2E spy vrstvou.
  • U admin useActionState formulářů, které mění booking stav, nenechávej response čekat na externí síťové side-effecty (např. Pushover API). Notifikace posílej non-blocking (void ...catch(...)), aby se UI nezaseklo na pending při pomalé třetí straně.
  • U useOptimistic (React 19 / Next.js 16) dispatchuj optimistic update pouze uvnitř startTransition(...) nebo action contextu. Přímé volání mimo transition v event helperu vede k runtime warningu An optimistic state update occurred outside a transition or action.
  • Veřejné booking routy nově obsahují i bezpečný provozní action flow /rezervace/akce/[intent]/[token], který renderuje serverovou validaci tokenu a klientský potvrzovací panel nad server action submittem.
  • Veřejné API nyní obsahuje i route handler /api/calendar/owner.ics, který vrací chráněný .ics feed pro Apple Calendar subscription; endpoint je veřejný jen přes tajný token v URL a nepoužívá session auth.
  • Klasické admin POST route handlery (/api/auth/login, /api/auth/logout, owner resend invite, admin voucher lookup POST) nespoléhej jen na SameSite=Lax; drž explicitní Origin/trusted host kontrolu přes isSameOriginAdminRequest(...), aby cross-origin form submit skončil dřív než auth nebo mutace.
  • Veřejný GET /api/health/live je bez-DB liveness probe. Veřejný GET /api/health je readiness probe s jediným SELECT 1 a minimálním JSON kontraktem; nesmí číst frontu ani vracet worker, incident nebo release metadata.
  • Detailní GET /api/health/diagnostics zachovává provozní snapshot fronty, workeru, incidentů a releasu, ale musí zůstat dostupný pouze owner session. Selhání detailního Prisma read modelu vrací ownerovi 200/warning s EMAIL_HEALTH_UNAVAILABLE a detail chyby se loguje pouze serverově.
  • DB failure alert z health endpointu je best-effort non-blocking dispatch s vlastním desetiminutovým in-memory cooldownem. Nečekej na Pushover v requestu; cooldown je lokální pro jeden runtime proces, proto v multi-instance provozu není náhradou centrálního alertingu.
  • /rezervace používá connection() a renderuje se request-time, aby ručně publikované sloty nebyly zafixované do build outputu.
  • Veřejné SEO landing pages (/, /sluzby, /cenik, /vouchery, /o-mne, detail /sluzby/[slug]) naopak nenuť do request-time režimu jen kvůli read modelům nebo JSON-LD. Next.js 16 výslovně doporučuje nedávat await connection() přímo do page komponenty, pokud tím jen zbytečně bráníš statickému shellu a metadata nepotřebují runtime request kontext.
  • Klientské UX booking flow je soustředěné v src/features/booking/components/booking-flow.tsx, ale rychlé decision bloky jsou rozsekané do menších komponent:
    • CategorySelect pro první rozhodnutí nad kategoriemi
    • SuggestedSlots pro nejbližší jedním klikem rezervovatelné časy
    • StickyCTA pro mobilní pokračování / submit bez ztráty kontextu
    • BookingConfirmationPanel pro post-submit stav se status blokem, dominantním termínem, CTA a kontaktem
  • Veřejný katalog slotů v src/features/booking/lib/booking-public/catalog.ts nově používá helper booking-slot-availability.ts, který:
    • slučuje navazující kompatibilní publikované sloty do jednoho delšího veřejného okna,
    • zachovává mapu původních segmentů pro správné slotId při submitu,
    • počítá bookedIntervals podle skutečných aktivních rezervací překrývajících daný čas, ne jen podle relace Booking.slotId.
  • Tokenová správa rezervace (/rezervace/sprava/[token]) volá katalog s vynecháním právě spravované rezervace. Díky tomu může nabídnout i dřívější start v navazujícím volném bloku před původním termínem; serverová validace ale dál kontroluje celý coverage řetězec a všechny ostatní aktivní bookingy.
  • Admin detail rezervace používá pro reschedule sloty stejný princip (excludeBookingId v getPublicBookingCatalog). Bez toho by vlastní aktivní booking blokoval původní slot a UI by nenabídlo posun o 30 minut dřív přes volné okno před začátkem.
  • Admin detail rezervace nyní podporuje i přímou výměnu služby na existujícím bookingu. Tato mutace musí zůstat serverově omezená na PENDING a CONFIRMED, přepočítat serviceDurationMinutes, cleanupMinutes, cleanupBlockMinutes, scheduledEndsAt, blockedUntil a zapsat audit do BookingStatusHistory.
  • Změna služby nesmí tiše obejít doménová omezení. Při další úpravě zachovej validaci proti stávajícímu času: nová služba se musí vejít do coverage aktuálního slotu nebo interního override, nesmí porušit allowedServices a nesmí projít přes navázaný službový voucher pro jinou službu.
  • Horní hlavička detailu rezervace drž jako kompaktní dvouřádkový toolbar: nahoře návrat + stav/kanál vlevo a rychlé akce vpravo, dole jméno klientky a sekundární řádek služba · délka · termín; termín nemá být vykreslený v samostatném velkém boxu.
  • Playwright booking fixture v tests/e2e/helpers/fixtures.ts seeduje termíny dynamicky podle aktuální booking policy (bookingMinAdvanceHours, bookingMaxAdvanceDays, bookingCancellationHours), aby testované self-service scenáře vždy běžely uvnitř skutečného online okna pro rezervaci/storno/přesun.
  • Protože E2E suite běží napříč více specy se 2 workery, fixture sloty se nesmí spoléhat na jeden sdílený čas. createCatalogFixture() vytváří trojici availability slotů v transakci, start rozhazuje hashovaně podle runId a při AvailabilitySlot_active_time_window_excl zkusí další kandidát.
  • Test runtime (NODE_ENV=test) záměrně vypíná Prisma client stdout/stderr query error logy v src/lib/prisma.ts; zachycené a retrynuté seed konflikty by jinak šuměly ve výstupu E2E/DB testů, i když samotný test projde.
  • U E2E výběru slotů nepočítej s unikátním textovým labelem času; při stejných časech v různých blocích je selector podle textu (.first()) flaky. V testech ověřuj vybraný hidden slotId proti očekávané fixture hodnotě.
  • U booking management E2E helperů nepočítej s tím, že fixture předem uhádne přesný label nebo dokonce konkrétní nabízený slot. Sdílená test DB může do katalogu propsat jinou sadu volných časů; scénáře proto stav nad UI potvrzují přes hidden slotId/newStartAt vybraného skutečně dostupného tlačítka, ne přes pevný accessible name.
  • E2E fixture slot labely musí formátovat datum z konkrétního začátku daného slotu, ne ze začátku celé testovací dvojice. Při časech kolem půlnoci by jinak success slot hledal předchozí den a Playwright by mohl kliknout na stale slot ze staršího běhu.
  • Při vytvoření rezervace nebo přesunu nad coverage řetězcem už nestačí rozřezat jen single-slot případ. Když booking začíná uprostřed prvního segmentu nebo končí uprostřed posledního segmentu, engine musí rozdělit i tyto krajní coverage sloty, jinak admin planner falešně vykreslí volný okraj jako locked remainder.
  • Pro dočištění starších dat je v scripts/repair-legacy-chained-slots.mjs záměrně konzervativní repair flow:
    • opravuje jen plain published anchor sloty s jedinou navázanou rezervací,
    • umí vytvořit jen bezpečný before a/nebo after fragment bez zásahu do booking FK,
    • případy s více bookingy na jednom slotu nechává jako skipped, protože tam už nejde bez doménového rozhodnutí garantovat bezpečný automatický split.
  • Pro editovatelnost plain published slotu v planneru neber CANCELLED booking jako blokující. Pokud na slotu nezůstává aktivní nebo dokončená návštěva ani jiné omezení, planner ho má ukázat jako běžnou dostupnost. Write model ale nesmí historický slot smazat natvrdo; při publish mutaci ho archivuje a případný nový aktivní interval založí zvlášť.
  • Při slučování sousedních published slotů po storno/reschedule nejdřív vyřaď slučovaný sousední slot z active DB constraintu (AvailabilitySlot.status = ARCHIVED) a až potom rozšiřuj anchor interval. Opačné pořadí vyvolá PostgreSQL AvailabilitySlot_active_time_window_excl, protože se dvě aktivní okna na okamžik překryjí uvnitř jedné transakce.
  • Stav rezervace COMPLETED je provozní uzávěrka po proběhlé návštěvě, ne nástroj pro předběžné odbavení. Admin akce Hotovo smí projít až po scheduledEndsAt, protože aktivní blokace kapacity a dashboardové volné úseky počítají jen PENDING a CONFIRMED.
  • Dashboardová timeline dne načítá pro zobrazení i COMPLETED, aby hotové návštěvy zůstaly v dnešním plánu viditelné. Výpočet volných oken ale ořezává začátek na aktuální čas, takže minulá dostupnost se nikdy nepropíše jako akční volný termín.
  • Alerty v admin dashboardu (Vyžaduje pozornost) mají zůstat jen pro akční provozní problémy (pending bookings, failed e-maily, overdue rezervace). Samotná nulová dostupnost dnes/zítra se nesmí eskalovat jako warning/problem alert.
  • Empty state v Nejbližší volné termíny drž neutrální tón (Momentálně nejsou publikované žádné nadcházející volné termíny.). Pokud existují budoucí DRAFT sloty, ukaž i počet návrhů čekajících na publikování s přímým odkazem do plánovače dostupnosti.
  • Admin planner Volné termíny načítá pro zobrazení PENDING, CONFIRMED i COMPLETED; dokončené rezervace jsou vizuálně tlumené, ale stále vysvětlují, proč historický úsek není běžná editovatelná dostupnost.
  • Drag editace v planner gridu musí zůstat funkční i pro mobilní pointery touch/pen; nespoléhej jen na MouseEvent.buttons === 1 a u buněk drž touch-action: none, aby tažení nepřebíjel nativní scroll gesture.
  • UX polish týdního planneru drž výslovně jako density pass, ne jako novou IA: hlavní priorita je plocha pro grid, datum týdne jen jednou v toolbaru, pravý panel ve 3 kartách a legenda jako malý sekundární prvek až u detailu výběru.
  • Pro další úpravy planneru preferuj jemné změny kontrastu a rytmu mřížky před přidáváním dalšího chrome: výraznější celé hodiny, subtilnější půlhodiny, jasnější selected day/block a minimum textu přímo uvnitř gridu.
  • FullCalendar planner ukládá změnu dostupnosti průběžně přes applyPlannerSelectionAction(); při chybě nabídne opakování nebo obnovení uloženého stavu.
  • Planner mutace v src/features/admin/lib/admin-slots/mutations.ts běží serializable a musí počítat s retry na Prisma P2034 / TransactionWriteConflict; paralelní CI cleanup nebo jiný zápis do AvailabilitySlot tabulky nesmí shodit změnu dostupnosti na první konflikt.
  • Admin detail klientky je provozní CRM pohled nad getAdminClientDetailData(...) plus samostatné server actions pro Client.internalNote a kontaktní údaje. Při úpravách neměň route kontrakt /admin/klienti/[clientId] / /admin/provoz/klienti/[clientId] a nepřidávej nová pole do Prisma schematu kvůli preferencím. Úprava kontaktu musí validovat e-mail/telefon, hlídat unikátní Client.email a propsat e-mail/telefon jen do aktivních rezervací (PENDING/CONFIRMED), protože booking e-maily dál čtou Booking.clientEmailSnapshot; proběhlé rezervace (COMPLETED/CANCELLED/NO_SHOW) zůstávají auditně beze změny. Každý propis kontaktu do aktivní rezervace zapisuj do BookingStatusHistory (reason: Kontakt klientky upraven) včetně metadat původního/nového e-mailu a telefonu. Poslední návštěva v detailu znamená poslední minulou rezervaci ve stavu COMPLETED, ne Client.lastBookedAt, protože lastBookedAt se aktualizuje už při vytvoření rezervace. CRM souhrn počítej přes src/features/clients/lib/client-crm-summary.ts; pro platby nepíš novou aritmetiku mimo tento helper a sdílený getBookingPaymentSummary(...). Uhrazeno je součet skutečných plateb a voucherových čerpání, ale Neuhrazeno smí zahrnout jen COMPLETED rezervace nebo aktivní PENDING/CONFIRMED rezervace se začátkem v minulosti, aby budoucí termíny nevypadaly jako dluh. UI drž kompaktní jako provozní workspace: žádné hero měřítko, tmavé card UI, jemné bordery, malé uppercase labely a krátké řádky historie bez zbytečných placeholderů poznámek.
  • Stejnou definici drž i seznam klientek na /admin/klienti a /admin/provoz/klienti: sloupec i řazení Poslední návštěva se musí opírat o poslední minulou rezervaci ve stavu COMPLETED, ne o profilové Client.lastBookedAt.
  • Totéž platí pro starší read model getClientsData(...) v src/features/admin/lib/admin-data.ts, pokud se ještě někde renderuje fallback / legacy sekce klientek přes AdminSectionPage.
  • Root /admin/email-logy je kompatibilní redirect na /admin/logy?view=emails; e-mailový seznam patří do společného read-modelu getAdminLogsData(...). Route layout v src/app/(admin)/admin/email-logy/layout.tsx zůstává kvůli detailu [emailLogId]; do jeho potomka nepřidávej další AdminShell, protože Next.js rodičovské layouty vnořuje a výsledkem by bylo dvojité levé menu.
  • Obecný fallback AdminSectionPage už v routing vrstvě neexistuje. Pokud přidáváš novou admin sekci, přidej jí explicitní obsluhu do createAdminSectionRoute(...) nebo vlastní statický route soubor; nenechávej „dočasný“ generic renderer jako skrytý runtime fallback.
  • Intro copy v adminu drž provozně a konzistentně: krátký eyebrow jako kontext sekce, jednoduchý title jako pracovní název a jednovětý description bez marketingového tónu.
  • Nedrž mrtvé read-modely ani komponenty jen kvůli historii rout. E-maily, Pozornost, Události a Systém musí zůstat ve společném feedu s jednotnými filtry a stránkováním.
  • Historie návštěv v detailu klientky rozlišuje původ poznámek u rezervace: Klientka pro Booking.clientNote a Interně pro Booking.internalNote. Pokud existují obě, zobraz obě; nevracej se k prioritě internalNote ?? clientNote, protože ta ztrácí provozní kontext.
  • Seznam klientek v adminu je click-to-open: na desktopu otevírá detail celá tabulková řádka, na mobilu celá karta. Tlačítko/štítek Detail zůstává jen jako vizuální affordance stejné navigace.
  • Primární CTA Vytvořit rezervaci v detailu klientky musí pro OWNER i SALON vést do existujícího ručního booking workspace /admin/.../rezervace?create=1&clientId=.... Prefill klientky je jen UX zkratka: booking action i nadále musí projít stejnou server-side validací dostupnosti, překryvů, služeb, notifikací a audit trailu.
  • Stejný princip deep-link prefill teď používá i dashboard a planner: /admin/.../rezervace?create=1&date=YYYY-MM-DD&time=HH:MM otevře ruční booking drawer s předvyplněným ručním termínem, ale bez obcházení backend validace. Pokud měníš tento tok, drž query kontrakt create/clientId/date/time kompatibilní napříč dashboardem, plannerem a klientským detailem.
  • Dashboard Dnešní plán a karta Další rezervace záměrně obsahují přímé provozní akce Volat, E-mail a Nová rezervace. Cíl je méně klikání při obsluze nejbližších klientek; při dalších úpravách nepřeváděj tyto akce zpět jen do detailu rezervace.
  • Sekce Nejbližší volné termíny a DayInspector v planneru jsou nově akční, ne jen orientační. Volné okno nebo vybraný blok musí umět otevřít stejný ruční booking drawer s předvyplněným termínem, aby kosmetička nemusela čas přepisovat mezi sekcemi.
  • Stejný helper řeší i backend validaci souvislého pokrytí intervalu při createBookingWithEngine(...) a rescheduleBooking(...); když upravuješ pravidla slotů, drž veřejný katalog a backend coverage logiku v sync.
  • Coverage validace musí umět spadnout z preferovaného slotId na segment, který skutečně obsahuje nový začátek. Tohle je důležité pro posun rezervace na dřívější start přes volný blok před původním termínem.
  • Stabilizační refaktor z 2026-04-24 rozděluje dříve monolitické booking/admin soubory do menších interních modulů při zachování stávajících entrypointů:
    • src/features/booking/lib/booking-public.ts je façade nad booking-public/shared.ts, catalog.ts, engine.ts, notifications.ts
    • src/features/booking/components/booking-flow.tsx drží jen stav a orchestraci kroků; jednotlivé UI bloky jsou v src/features/booking/components/booking-flow/*
    • src/features/admin/lib/admin-slots.ts je façade nad admin-slots/time.ts, helpers.ts, queries.ts, mutations.ts, types.ts
  • Při dalším refaktoru těchto oblastí drž kompatibilní veřejné exporty v původních entrypointech, aby se nerozbily existující importy v actions, routech a admin UI.
  • src/app/robots.ts a src/app/sitemap.ts používají metadata route API v App Routeru.
  • robots.txt i sitemap.xml musí skládat Host, Sitemap a <loc> URL přes kanonický SEO origin siteConfig.canonicalUrl (NEXT_PUBLIC_SITE_URL s fallbackem na NEXT_PUBLIC_APP_URL), aby byly technické SEO routy konzistentní s JSON-LD a page metadata.
  • src/app/sitemap.ts je explicitně ISR metadata route (export const revalidate = 86400), takže se sitemap.xml generuje dynamicky a obnovuje nejvýše jednou denně bez plného redeploye.
  • U Next.js 16 route segment config exportů (revalidate, dynamic, fetchCache, atd.) používej v route souboru přímo literál nebo jinou staticky analyzovatelnou hodnotu; výraz typu 60 * 60 * 24 může při next build skončit chybou Invalid segment configuration export detected.
  • src/app/sitemap.ts musí nastavovat realistické lastModified hodnoty: detail služeb z Service.updatedAt, statické stránky ze stabilního data poslední obsahové revize (ne plošně new Date() pro všechny URL).
  • Veřejné stránky používají buildPageMetadata(...) a vždy předávají vlastní path; helper z něj skládá canonical a OpenGraph URL ze siteConfig.canonicalUrl. Nevkládej globální canonical do root layoutu, protože by ho zdědily podstránky.
  • Playwright smoke test pro veřejné stránky ověřuje, že canonical a og:url míří na stejnou stránkovou URL. Discovery test zároveň hlídá Host/Sitemap v robots.txt, sitemap <loc> na stejném originu včetně indexovatelné route /studio a absenci historické http://ppstudio.cz URL.
  • src/features/public/lib/public-services.ts nesmí předpokládat, že Prisma relation include vždy vrátí neprázdnou kategorii ve stejném okamžiku jako službu; při souběžném testovacím cleanupu může relation fetch krátce vrátit null, takže mapování veřejných služeb musí mít fallback kategorie a nesmí spadnout na .name.
  • Stránka /o-mne je v src/features/public/components/about-page.tsx záměrně vedená jako klidná premium landing page, ale density pass z 2026-05-03 drží kompaktnější rytmus: menší section py, střídmější card padding, těsnější návaznost bloků Můj příběh a Můj přístup, kompaktnější FOR LIFE & MADAGA pills a nižší certifikační gallery. Při dalších úpravách zachovej obsahovou skladbu a CTA routy z aboutContent, nevracej stránku do roztahaného hero/card rytmu.
  • Strukturovaná data jsou v src/features/public/components/seo-json-ld.tsx: veřejný layout vkládá BeautySalon/WebSite, homepage vkládá vlastní WebPage, stránka O mně přidává Person pro Pavlínu Pomykalovou a detail služby vkládá samostatně Service a BreadcrumbList. BreadcrumbList skládej přes buildBreadcrumbListJsonLd(...), aby se absolutní URL tvořily jednotně z kanonického SEO originu (siteConfig.canonicalUrl; NEXT_PUBLIC_SITE_URL s fallbackem na NEXT_PUBLIC_APP_URL) bez zdvojených lomítek. JSON-LD se serializuje přes JSON.stringify(...).replace(/</g, "\\u003c"); adresa salonu v BeautySalon a areaServed u služby se berou z getPublicSalonProfile() / SiteSettings, aby držely stejnou hodnotu jako veřejný kontakt. BeautySalon navíc obsahuje geo s přesnými souřadnicemi studia. Service.offers se generuje jen pro jasně číselnou cenu služby.
  • public/llms.txt je veřejný strojově čitelný rozcestník pro LLM crawlery. Drž ho stručný, faktický a stabilní: hlavní landing pages, kanonický web, kontaktní fakta a výslovné upozornění, že admin/tokenové URL nejsou určeny k citaci ani navigaci.
  • src/config/site.ts drží globální fallback SEO popis a kontakty pro metadata a technické routy. Udržuj ho věcný k PP Studiu ve Zlíně a nepoužívej placeholder kontakty; produkční fallback je info@ppstudio.cz, +420 732 856 036 a Sadová 2, 760 01 Zlín.
  • src/app/robots.ts je v produkčním nastavení otevřený pro celý veřejný web (Allow: /); blokované mají zůstat jen neveřejné admin a tokenové self-service cesty. Noindex veřejné stránky bez tokenu v path neblokuj v robots.txt, aby si robot mohl přečíst jejich metadata.
  • Veřejné JSON-LD upravuj přes src/features/public/components/seo-json-ld.tsx; data ber z public read modelů a getPublicSalonProfile(), ne z duplicitních hardcoded kontaktů. Testuj serializer i helpery v seo-json-ld.test.ts, zejména CZK cenu, ISO duration, BreadcrumbList URL a absenci prázdných hodnot.
  • Web Vitals reporting drž izolovaný v WebVitalsReporter; neposílej URL, klientská data ani booking tokeny. Pokud bude potřeba jiný backend než Matomo, nejdřív přidej ADR a bezpečnostní návrh endpointu.
  • Admin routy pod src/app/(admin)/admin dědí explicitní metadata.robots noindex/nofollow z admin layoutu. Nové admin obrazovky drž pod tímto stromem; pokud vznikne neveřejná route mimo něj, musí dostat vlastní noindex metadata.
  • Matomo client tracking je v src/features/analytics/* a inicializuje se přes src/components/layout/site-shell.tsx. Při lokálním vývoji nastav NEXT_PUBLIC_MATOMO_ENABLED=true, NEXT_PUBLIC_MATOMO_URL a NEXT_PUBLIC_MATOMO_SITE_ID; bez kompletní konfigurace je helper bezpečný no-op. Web Vitals mají navíc samostatný flag NEXT_PUBLIC_WEB_VITALS_ENABLED (default true), takže je lze vypnout bez vypnutí celého Matomo trackingu.
  • Clarity client tracking je v src/features/analytics/clarity*.ts(x) a inicializuje se také přes SiteShell (next/script, lazyOnload); bez kompletní konfigurace (NEXT_PUBLIC_CLARITY_ENABLED, NEXT_PUBLIC_CLARITY_PROJECT_ID) je helper bezpečný no-op.
  • Google Ads client tracking je v src/features/analytics/google-ads*.ts(x) a inicializuje se také přes SiteShell (next/script, afterInteractive); bez kompletní konfigurace (NEXT_PUBLIC_GOOGLE_ADS_ENABLED, NEXT_PUBLIC_GOOGLE_ADS_ID) je helper bezpečný no-op.
  • Meta Pixel client tracking je v src/features/analytics/meta-pixel*.ts(x) a inicializuje se také přes SiteShell (next/script, afterInteractive); bez kompletní konfigurace (NEXT_PUBLIC_META_PIXEL_ENABLED, NEXT_PUBLIC_META_PIXEL_ID) je helper bezpečný no-op.
  • Meta Pixel helper v src/features/analytics/meta-pixel.ts je zároveň jediné místo pro fbq eventy a sanitizaci payloadu; nové eventy nepřidávej jako ad-hoc window.fbq(...) v komponentách.
  • SiteShell před renderem MatomoTracker čte přítomnost admin session cookie ppstudio-admin-session; pokud je cookie přítomná, MatomoTracker se renderuje s disabled a nenačte matomo.js ani init script. Cíl je vyloučit vlastní návštěvy administrace i na veřejných routách bez DB dotazu.
  • Stejný disabled guard v SiteShell používá i ClarityTracker, takže přihlášený admin není měřený ani na veřejných stránkách.
  • Stejný disabled guard v SiteShell používá i GoogleAdsTracker, takže přihlášený admin není měřený ani na veřejných stránkách.
  • Stejný disabled guard v SiteShell používá i MetaPixelTracker, takže přihlášený admin není měřený ani na veřejných stránkách.
  • GoogleAdsTracker v App Routeru neposílá jen úvodní inline snippet; po klientské navigaci opakuje gtag('config', tagId, { page_path, page_title }), aby se pageview neztratily při přechodech bez full reloadu. page_path se skládá přes stejnou sanitizaci jako Matomo, takže nepropustí tokenové self-service URL ani citlivé query parametry.
  • MatomoTracker bootstrapuje _paq přes inline next/script na strategii afterInteractive, aby se bezpečný setCustomUrl spolehlivě propsal i v pomalejších CI/prohlížečových bězích ještě před prvním self-service klikem. Externí matomo.js se načítá stejnou strategií. Inline skript pageview neposílá; první načtení i App Router navigace mají jediného odesílatele v klientském layout efektu.
  • Pageview tracking v App Routeru poslouchá usePathname() a useSearchParams(). Layout efekt posílá první i další povolené pageview se sanitizovanou URL, bezprostředně před každým trackPageView ji znovu zapíše do Matomo fronty a deduplikuje podle poslední skutečně odeslané cesty; inline bootstrap pouze nastaví tracker a bezpečnou URL.
  • Tokenové self-service booking route (/rezervace/sprava/*, /rezervace/storno/*, /rezervace/akce/*) neposílají pageview s tokenem. MatomoTracker na nich může pouze inicializovat _paq, aby šly z klientských handlerů poslat bezpečné neosobní eventy bez raw URL.
  • Funnel a CTA eventy se smí volat jen v client handlerech nebo efektech po úspěšné akci; neposílej jména, e-maily, telefony, poznámky, tokeny ani raw URL s citlivými parametry.
  • Meta Pixel standardní eventy drž anglicky podle konvence platformy (PageView, ViewContent, InitiateCheckout, Schedule); custom Meta eventy používej stabilně v PascalCase (BookingServiceSelected, BookingDateSelected, BookingTimeSelected, BookingContactStarted).
  • Meta Pixel v aktuální verzi měří tyto neosobní kroky:
    • detail služby: ViewContent
    • výběr termínu nebo přechod do kontaktního kroku booking flow: InitiateCheckout
    • výběr služby: BookingServiceSelected
    • výběr dne: BookingDateSelected
    • výběr času: BookingTimeSelected
    • první interakce s kontaktním krokem: BookingContactStarted
    • úspěšně vytvořená rezervace: Schedule (bez value a currency, pokud není k dispozici jednoznačná finální cena)
  • Matomo eventy pojmenovávej primárně česky (category i action) a drž je stabilní v čase; anglické názvy používej jen tam, kde jde o standardní technický termín (Web Vitals, CLS, LCP). U Rezervace / Kontakt pole chyba nese name jen název pole a bezpečný důvod (povinné, příliš krátké, neplatný formát), nikdy hodnotu ani text chyby.
  • V kontaktním kroku rezervace (BookingContactStep) se sledují jen neosobní interakce s poli (fokus, začátek vyplnění, chyba) a název pole (fullName/email/phone); nikdy neposílej obsah zadaných hodnot.
  • Server-side dashboard reporting je v src/lib/analytics/matomo.ts a je záměrně oddělený od klientského helperu. Používá pouze MATOMO_* env bez NEXT_PUBLIC_, import "server-only", Matomo metody VisitsSummary.get, Events.getAction, Referrers.getReferrerType a Referrers.getCampaigns, a každý request cachuje přes fetch(url, { next: { revalidate: 300 } }).
  • getDashboardAnalytics() skládá první funnel krok viewed z Matomo pageview reportu Actions.getPageUrls pro /rezervace a /rezervace?..., zatímco kroky service, term, contact, submitted, created mapuje z event labels Rezervace / Služba vybrána, Rezervace / Čas vybrán, Rezervace / Kontakt zahájen, Rezervace / Odeslána rezervace a Rezervace / Vytvořena; protože Events.getAction může vracet jen action label, agregace akceptuje i zkrácené labely bez prefixu Rezervace / .... Hlavní KPI conversions je stejné číslo jako funnel.created, aby se metrika rezervací nerozcházela s funnel krokem.
  • getDashboardAnalytics() vrací i contactStepQuality nad eventy Rezervace / Kontakt zahájen, Kontakt pole fokus, Kontakt pole vyplnění začátek, Kontakt pole chyba; API počítá počty i poměry focusRate, inputRate, errorRate vůči Kontakt zahájen.
  • Ve veřejném booking flow drž dva typy Matomo měření odděleně: pageview /rezervace jako vstup do funnelu a potom hlavní funnel eventy (Služba vybrána, Čas vybrán, Kontakt zahájen, Odeslána rezervace, Vytvořena) plus diagnostické/provozní mikro-eventy (Datum vybráno, Formulář chyba, Termín konflikt při odeslání, Bez služeb, Bez termínů, Kontakt pole fokus, Kontakt pole vyplnění začátek, Kontakt pole chyba). Do event name neposílej obsah formuláře ani raw chybové texty.
  • Doporučené termíny používají dva privacy-safe Matomo eventy: Rezervace / Doporučené termíny zobrazeny / service-slug se pošle jednou pro skutečně viditelnou konkrétní sadu a Rezervace / Doporučený termín vybrán / YYYY-MM-DD | HH:mm–HH:mm | service-slug | pozice N pouze při skutečném kliknutí klientky na doporučenou kartu, nikdy jen proto, že se čas vybraný v kalendáři shoduje s canonical key doporučení. Mobilní zobrazení zahrnuje první čtyři karty, desktop až šest; impression a pozice proto vždy vycházejí z aktuálně viditelné sady. Podíl eventových výběrů doporučení je Doporučený termín vybrán / Čas vybrán; eventové CTR doporučení je Doporučený termín vybrán / Doporučené termíny zobrazeny. Nejde automaticky o podíl unikátních klientek ani sessions.
  • Služba předvyplněna je čistě diagnostický event pro vstup z ceníku/detailu služby a nesmí zároveň znovu zapisovat Služba vybrána; funnel krok service má reprezentovat skutečné ruční potvrzení služby ve formuláři. Stejně tak Datum vybráno v booking flow posílej jen při změně konkrétního dateKey, ne znovu při kliknutí na čas ve stejném dni.
  • Pro tokenové self-service route drž stejné bezpečnostní pravidlo: lze měřit jen neosobní lifecycle kroky jako Správa rezervace / Změna termínu otevřena, Správa rezervace / Datum vybráno, Správa rezervace / Čas vybrán, Správa rezervace / Změna termínu odeslána, Rezervace / Storno odesláno a Rezervace / Storno dokončeno, ale nikdy ne token, plnou URL, jméno klientky ani kontakt. Privacy regex v src/features/analytics/matomo.ts nesmí blokovat samotná business slova typu storno; filtrovat má jen PII a raw tokenové cesty.
  • E2E krytí pro tento guard je v tests/e2e/booking-flows.spec.ts: Playwright stubuje matomo.js, sbírá _paq volání a ověřuje, že /rezervace/storno/[token] pouze inicializuje safe URL bez trackPageView, ale po potvrzení storna odešle Storno odesláno a Storno dokončeno bez raw tokenu.
  • Pokud /rezervace vstupuje s validním query prefill service=..., posílej vedle standardního Služba vybrána i samostatný event Služba předvyplněna; tím zůstane starý funnel kompatibilní, ale Matomo reporty umí oddělit CTA z ceníku/detailu služby od ruční volby až v booking flow.
  • API endpoint src/app/api/admin/analytics/route.ts vrací tuto agregaci jako JSON. Route je chráněná přes getSession() a pustí jen role OWNER / SALON; bez session vrací 403, při interní chybě vrací 200 s bezpečným nulovým fallbackem a em dash topSource.
  • src/components/admin/AnalyticsWidget.tsx je klientská komponenta pro admin dashboard. Na mountu volá fetch('/api/admin/analytics'), validuje shape payloadu v runtime, zobrazuje Načítání… / Data nejsou dostupná a po úspěchu vykreslí jen kompaktní souhrn Výkon webu (návštěvy dnes, rezervace dnes, míra rezervace). Detail zdrojů a funnel kroků je dostupný až v rozbalení Zobrazit analytiku, aby hlavní cockpit neukazoval matoucí procenta jako primární provozní signál.
  • DashboardPage je kompaktní denní provozní cockpit. Priorita stránky je Provozní přehled -> Vyžaduje pozornost -> provozní KPI -> Dnešní plán / Nejbližší volné termíny -> Rychlé akce / Tento týden / Výkon webu; horní provozní blok má fungovat jako nízká operační lišta, KPI jako jeden metric strip s krátkým detailem metriky a pravý sloupec jako krátký podpůrný panel. Disabled stav Matomo není nakonfigurované. se rozhoduje na serveru podle přítomnosti MATOMO_URL, MATOMO_SITE_ID a MATOMO_AUTH_TOKEN; klient kvůli tomu nečte žádný secret.
  • Sekce dashboardu Vyžaduje pozornost se renderuje pouze při existenci actionable alertů (alert.emphasis !== "ok"); pokud jsou jen ok stavy, komponenta vrací null. Komponenta musí použít alert.emphasis: primary alert je vizuálně hlavní, sekundární alerty jsou menší podpůrné položky. Pokud primární alert neexistuje, jako hlavní se použije první actionable alert.
  • Když se sekce Vyžaduje pozornost zobrazí, musí být mobilně odolná: text alertu se zalamuje (break-words) a CTA může být pod textem; nepoužívej zde truncate v řádku s CTA, protože to způsobuje horizontální přetečení karty na úzkých iOS viewports.
  • Actionable alert pravidla v getAdminDashboardData(...) jsou záměrně provozní a konzervativní: pending-bookings, email-failures a current-overdue (aktivní rezervace po konci termínu). Samotná nulová dostupnost dnes/zítra nebo nízká týdenní kapacita se v této sekci neeskalují jako problém.
  • Rychlé akce v dashboardu neduplikují horní primární CTA Vytvořit rezervaci; data read modelu drží podpůrné vstupy bookings, availability, clients, vouchers. Pokud se sem přidá další akce, musí být provozně podpůrná a nesmí soutěžit s hlavní CTA v hero liště.
  • Admin sekce Rezervace po density refaktoru používá jeden horní control panel: quick stats (stat query param) a formulářové filtry query/status/source/dateFrom/dateTo sdílejí stejnou kartu a dál zůstávají plně URL-driven. source filtr je kanál vytvoření rezervace, ne UTM/referrer akvizice. Na mobilu panel není sticky (kvůli překryvu seznamu), sticky zůstává od md výš.
  • Stejná sekce nově drží i URL-driven progresivní odkrývání seznamu: showPast=1 rozbalí historickou skupinu a needsClosureLimit/pendingLimit/upcomingLimit/pastLimit řídí, kolik položek je vidět v jednotlivých blocích. Pokud upravuješ toolbar nebo odkazy Zobrazit další, zachovej tyto query parametry i při další filtraci.
  • Search input v rezervacích používá malý klientský combobox s debounce POST fetch do /api/admin/bookings/search; hledaný text patří výhradně do JSON body, nikdy do URL. Endpoint musí zůstat admin-only, same-origin a vracet bezpečné textové návrhy bez přímé mutace URL nebo DB. Pro běžný text nabídne hlavně fullName a název služby; kontaktní návrhy (e-mail/telefon) se mají ukázat jen když dotaz vypadá jako kontakt. Odpověď je vždy Cache-Control: private, no-store.
  • Mobilní layout tohoto control panelu je citlivý na implicitní minimální šířku CSS grid items a nativních input[type="date"]. Labely, inputy/selecty i akční řádek proto drž min-w-0; nepřidávej pevné mobilní šířky, které by znovu vytvořily horizontální přetok v admin shellu.
  • Veřejný SiteHeader přepíná na desktop navigaci až od lg; md viewporty (včetně iPad portrait) mají zůstat v kompaktním režimu s mřížkovou navigací a mobilním CTA, aby nedocházelo k useknutí pravého desktop tlačítka.
  • Rezervační KPI strip se počítá serverově v getReservationsData(...) bez nového persistence modelu:
    • Čeká na potvrzení: Booking.status = PENDING
    • Dnes: všechny rezervace se scheduledStartsAt v dnešním dni
    • Tento týden: všechny rezervace v aktuálním týdnu počítaném od pondělí
    • Bez kontaktu: rezervace, kde je clientEmailSnapshot = "" a zároveň clientPhoneSnapshot prázdný nebo null
  • Read model seznamu rezervací už nepoužívá Dnes / Zítra / Později / Dříve; seskupení je needs_closure / pending / upcoming / past. needs_closure vítězí jako první a používá scheduledEndsAt < now plus aktivní stavy PENDING/CONFIRMED, aby proběhlé návštěvy čekající na uzavření nezapadly mezi minulými rezervacemi. pending pak drží budoucí čekající rezervace, upcoming ostatní aktivní budoucí rezervace a past historické i uzavřené stavy (COMPLETED, CANCELLED, NO_SHOW).
  • Souhrnný počet výsledků v rezervacích se nesmí odvozovat od viditelného výřezu nebo interního findMany limitu. Read model má počítat summary.totalCount samostatně přes count(where) a teprve potom nad celým výsledkem aplikovat skupinové limity pro UI.
  • Admin login /admin/prihlaseni má zůstat krátký a netechnický: neukazuj interní role, session ani bootstrap účty v copy/placeholderu a zachovej výrazný focus-visible stav polí i submit tlačítka.
  • Dashboard analytics API vrací také periodLabel a sources. Backend čte Referrers.getCampaigns pro kampaně a při absenci kampaní používá Referrers.getReferrerType; labely mapuje na business názvy Instagram, Firmy, Google, Přímý vstup, případně Offline/Ostatní. Rezervace u zdrojů jsou odhad: počet Rezervace / Vytvořena se rozdělí podle podílu návštěv jednotlivých zdrojů a UI je proto označuje jako odhad, ne přesnou atribuci.
  • /api/admin/analytics teď vrací i reportingStatus a volitelně reportingMessage, aby frontend poznal rozdíl mezi reálnou nulou a problémem v Matomo reportingu. Lokální CLI kontrola používá npm run analytics:check; sahá jen na serverové MATOMO_* env a nevystavuje token klientovi.
  • Owner Pushover notifikace maji Next.js server-only wrapper v src/lib/notifications/pushover.ts a sdilenou worker-safe implementaci v src/lib/notifications/pushover-core.ts. Verejne API je sendOwnerPushover(input), plus interni helpery pro booking a email failure zpravy; vsechny chyby se catchuji a loguji, aby Pushover nikdy nerozbil transakci rezervace, doruceni emailu ani reminder scan.
  • V admin read modelu e-mail logu drzi recentEmails[].trackingStateValue stejny union kontrakt jako deriveTrackingState(...), tedy vcetne processing; pri doplneni noveho tracking stavu aktualizuj vzdy obe strany najednou, jinak next build spadne na nekompatibilnich typech.
  • Pushover HTTP volani jsou navic omezena 3s timeoutem pres AbortSignal.timeout(...), aby pomala externi API nezdrzovala user-facing booking server actions a CI E2E scenare.
  • Pokud Pushover vola kod nacitany standalone skriptem (email:worker, jednorazove CLI), importuj @/lib/notifications/pushover-core, protoze import "server-only" je urceny pro Next.js bundler a v plain Node procesu by shodil start.
  • Per-user nastaveni drzi model UserNotificationSettings s vazbou 1:1 na AdminUser. Dotazy na prijemce vzdy filtruji AdminRole.OWNER, isActive = true, pushoverEnabled = true, vyplneny pushoverUserKey a zapnuty konkretni event toggle; SALON se nema nikdy objevit ani v UI, ani v odesilacim dotazu.
  • Obecný Pushover rate limit zůstává jednoduchý in-memory guard v procesu: kombinace type + bookingId/contextId/emailLogId se smí odeslat maximálně jednou za 30 sekund. Výjimkou je veřejný booking PUBLIC_BOOKING_RATE_LIMITED, který používá databázový atomický cooldown 10 minut podle bezpečného sourceHash a funguje i po restartu nebo mezi instancemi.
  • Pushover volani patri pouze do dulezitych side-effect bodu po uspesne domenove zmene: createBookingWithEngine, admin/email potvrzeni nebo zruseni, public storno, centralni rescheduleBooking, finalni selhani deliverEmailLog, selhani enqueue reminderu a vybrane neocekavane systemove chyby. Nepouzij ho pro pageviews, Matomo eventy, admin kliky ani bezne odeslani emailu.
  • U NEW_BOOKING Pushover zpravy dopln Klientka: Nova klientka nebo Klientka: Vracejici se klientka podle existence rezervace stejne clientId pred aktualni rezervaci. Neodvozuj tento udaj jen z aktualniho jmena, e-mailu ani telefonu a nepridavej do zpravy kontakt klientky.
  • Pro neocekavane serverove pady preferuj shared helper sendOwnerSystemErrorPushover(...) misto ad-hoc skladani SYSTEM_ERROR payloadu. Helper drzi jednotny typ alertu, pridava zkraceny summary Error.message a zachovava stavajici 30s rate-limit podle contextId.
  • Aktualni provozni scope SYSTEM_ERROR alertu zahrnuje verejne i admin booking flow (create/reschedule), public booking schema drift, fail enqueue navazneho reschedule emailu, planner mutace volnych terminu, voucher create/update/cancel + voucher email queue, owner invite resend flow a vybrane admin/API endpointy typu health nebo analytics. Naopak validacni chyby, business konflikty a cizi neplatne requesty se notifikovat nemaji.
  • Booking e-maily v src/lib/email/templates.ts sdílejí email design systém: 600px shell, inline styly, prezentační tabulky, systémové fonty pro CTA, jednotné bloky Služba / Datum / Čas, Místo, Kontakt a tlumenou patičku. Další šablony napojuj na tyto helpery místo ručního skládání vlastního layoutu.
  • Klientské booking e-maily berou salon kontakt z getPublicSalonProfile() a getEmailBrandingSettings(): název, adresa, telefon a e-mail jdou primárně ze SiteSettings, mapový odkaz se skládá přes Google Maps query z aktuálního názvu a adresy. Pevné údaje PP Studia (PP Studio, Sadová 2, 760 01 Zlín, info@ppstudio.cz, +420 732 856 036) smějí zůstat jen jako fallback při chybějícím nastavení nebo DB chybě.
  • Kontakt ani pomocné věty v klientských booking e-mailech neduplikuj v intro, CTA ani patičce. Text/plain varianta musí zůstat plnohodnotná a všechny uživatelské hodnoty v HTML vždy escapuj přes lokální helper, zejména jméno klientky, službu, voucher kód a klientskou poznámku v admin e-mailu.
  • Lokální náhledy booking šablon generuje npm run email:previews; výstup je v tmp/email-previews a skript nepřidává žádnou novou knihovnu ani nezapisuje do EmailLog.
  • Klientský e-mail booking-approved-v1 se renderuje v src/lib/email/templates.ts a má zůstat krátký, email-safe a mobilně čitelný: potvrzení rezervace, termín, služba, viditelná adresa, připomenutí .ics přílohy, jednorázový kontakt na studio a sekundární odkazy na správu rezervace dole.
  • Veřejně dostupná nahraná média se servírují přes route handler src/app/media/[kind]/[[...path]]/route.ts, ne přes public/ repozitáře.
  • next.config.ts používá allowedDevOrigins pro lokální LAN vývoj na 192.168.0.143 i pro public dev test přes ppstudio.cz / www.ppstudio.cz; bez toho Next.js 16 z jiného zařízení nebo přes reverse proxy zablokuje dev assety a HMR endpoint /_next/webpack-hmr.
  • npm test a npm run test:db:booking běží s node --import ./src/test/register-server-only.mjs --import tsx --test ..., takže plain Node test runner umí načíst import "server-only" bez zásahu do ostatních Next internals. Pokud přidáš další server-only moduly, použij tenhle sdílený hook místo lokálních per-test stubů.
  • Veřejné vytvoření rezervace (createBookingWithEngine) používá retry smyčku pro Prisma P2034 serializační konflikty. Aktuálně je limit MAX_BOOKING_TRANSACTION_RETRIES = 5 a mezi pokusy je krátký lineární backoff, aby se snížila flakiness při paralelních DB testech i v produkční špičce.
  • V Prisma 7 s @prisma/adapter-pg může stejný serializační konflikt probublat jako DriverAdapterError s cause.kind = "TransactionWriteConflict" místo PrismaClientKnownRequestError(P2034). Retry helpery v booking create/reschedule/cancel flow musí brát oba tvary jako retryable.
  • Kontaktní krok veřejné rezervace (booking-flow/contact-step.tsx) musí zachovat explicitní id/htmlFor vazby, stabilní ID pro hint/error texty a aria-describedby na všech polích; chybové texty oznamuj přes aria-live="polite" nebo role="alert" a klávesnicový focus-visible stav drž v akcentu PP Studia.
  • Běžné platby k rezervaci jsou samostatný ledger BookingPayment; nepřepisuj jím voucher redemption model ani individuální slevu. Stav úhrady se neukládá do DB, počítá ho getBookingPaymentSummary(...) z efektivní ceny rezervace (Booking.finalPriceCzk ?? Booking.servicePriceFromCzk), VoucherRedemption.amountCzk a BookingPayment.amountCzk. Server actions pro zápis/mazání plateb jsou v src/features/booking/payments/actions/booking-payment-actions.ts a UI patří přímo do existující sekce Úhrada v detailu rezervace. SERVICE voucher je nárok na službu, takže jeho čerpání nemá přebírat individuální cenu rezervace jako hodnotovou slevu.
  • Voucher doména je v src/features/vouchers a zůstává oddělená od admin UI, PDF i public booking flow. Entry body:
    • lib/voucher-code.ts generuje a normalizuje kódy PP-YYYY-XXXXXX.
    • lib/voucher-validation.ts vrací bezpečný public validační výsledek bez citlivých polí; verifyVoucherPublic(...) je určený pro samostatnou veřejnou kontrolu bez vazby na službu a bez jakéhokoli uplatnění.
    • lib/voucher-redemption.ts provádí admin uplatnění v transakci, zapisuje VoucherRedemption a blokuje další voucher na rezervaci, která už má alespoň jedno čerpání.
    • lib/voucher-management.ts drží server-side doménové funkce pro vytvoření, validaci a uplatnění bez klientských komponent.
  • Voucher doménové moduly nesmí mít top-level "use server". Veřejně volatelné server actions patří do src/features/admin/actions/*, kde každá action znovu ověřuje session a roli, než zavolá voucher doménu.
  • CreateVoucherInput a RedeemVoucherInput jsou inferované ze Zod schémat s transformacemi optionalText, takže v testech je potřeba explicitně předávat i klíče, které mají být prázdné (undefined) - typicky purchaserName, recipientName, message, internalNote a note.
  • Efektivní expirace voucheru je aplikační read pravidlo přes getEffectiveVoucherStatus(...); validace ani read modely automaticky nepřepisují DB status na EXPIRED.
  • Admin seznam voucherů je základní přehledová UI vrstva nad voucher doménou:
    • route factory obsluhuje /admin/vouchery pro OWNER i /admin/provoz/vouchery pro SALON,
    • navigace a guard berou vouchery jako sdílenou admin sekci,
    • stránka používá src/features/admin/lib/admin-vouchers.ts jako read model a src/features/admin/components/admin-vouchers-page.tsx jako prezentační vrstvu,
    • desktop používá skutečnou kompaktní tabulku jako primární fokus a menší šířky přecházejí na voucher karty,
    • create CTA je v nízké page header liště vpravo, metriky jsou v nízkém stripu a filtry q, type, status zůstávají URL-driven,
    • KPI strip je serverový read-model souhrn: Zbývá k uplatnění sčítá jen otevřené VALUE zůstatky a počet otevřených SERVICE voucherů, Brzy expirují sleduje otevřené vouchery s validUntil do 30 dnů,
    • badge stavů musí být vizuálně součástí řádku ve sloupci Stav; nevyváděj je mimo tabulku ani z nich nedělej druhotný seznam,
    • query parametry jsou q, type a status; filtr stavu musí odpovídat efektivnímu voucher statusu, ne jen hodnotě uložené v DB.
  • Statické voucher routy /admin/vouchery/* a /admin/provoz/vouchery/* mají vlastní layout.tsx exportující AdminShellLayout; nové statické admin routy mimo dynamický [section] wrapper musí dostat stejný layout, jinak se vykreslí mimo tmavý admin shell.
  • Detail voucheru běží přes konkrétní routy /admin/vouchery/[voucherId] a /admin/provoz/vouchery/[voucherId], route factory createAdminVoucherDetailRoute(...), admin wrapper getAdminVoucherDetailData(...) a komponentu src/features/admin/components/admin-voucher-detail-page.tsx. Prezentační vrstva má být provozní workspace: jedna summary karta nahoře, pod ní layout Parametry voucheru + Historie uplatnění vlevo a Kupující a odeslání + Poslední e-mailové pokusy vpravo. Nepřidávej další vysoké samostatné karty a nerozšiřuj kvůli detailu klientskou hranici celé stránky.
  • Provozní editace voucheru je oddělená v panelu src/features/admin/components/admin-voucher-operations-panel.tsx a server actions src/features/admin/actions/voucher-actions.ts. Smí měnit jen bezpečné provozní údaje purchaserName, purchaserEmail, validUntil a internalNote; kód, typ, hodnota, služba, měna, čerpání a PDF identita zůstávají neměnné.
  • Voucher se ruší přes stav VoucherStatus.CANCELLED, cancelledAt, cancelledByUserId, cancelReason a updatedByUserId; záznam se nemaže. Zrušení je povolené jen pro voucher bez VoucherRedemption, OWNER i SALON mají pro tyto provozní voucher akce stejná práva a veřejné ověření nesmí ukázat interní důvod zrušení.
  • Detail voucheru má navíc read-only sekci Odeslání e-mailem, která z EmailLog typu VOUCHER_SENT vrací jen bezpečnou historii posledních 5 záznamů. Read model nesmí vracet raw payload, processingToken, providerMessageId ani jiná technická pole; pokud dotaz na historii selže, detail voucheru má dál fungovat a jen zobrazit prázdný stav.
  • PDF voucheru běží přes route handlery /admin/vouchery/[voucherId]/pdf a /admin/provoz/vouchery/[voucherId]/pdf, sdílený handler createAdminVoucherPdfRoute(...), Next.js wrapper src/features/vouchers/lib/voucher-pdf.ts se import "server-only" a worker-safe core src/features/vouchers/lib/voucher-pdf-core.ts. Handler vždy ověřuje session a roli OWNER nebo SALON, PDF neukládá do DB ani na disk a generuje ho z aktuálního read modelu voucheru.
  • Voucherová šablona je po bootstrapu výhradně perzistentní záznam VoucherTemplate s privátním masterem v MEDIA_STORAGE_ROOT; každý voucher ukládá přesný templateKey i templateId a renderer historického voucheru nefallbackuje na aktuální default. Seed classic-v1.pdf je neveřejný bootstrap asset v src/features/vouchers/bootstrap-assets/, který je explicitně zahrnutý do Next output-file trace. Nový design znamená nový key (classic-v2, christmas-v1), nikoli změnu geometrie classic-v1; skutečný runtime master se nikdy nenačítá z public/.
  • classic-v2.pdf je připravený ColorPoint/Fujifilm Revoria uncoated kandidát s embedded profilem z oficiálního ColorPoint URL; jeho finální schválení tiskárnou, instalace do persistentní VoucherTemplate a přepnutí defaultu jsou samostatné rollout kroky. Reprodukční příprava používá npm run voucher:templates:prepare-classic-v2 -- classic-v1.pdf profil.icc classic-v2.pdf a profil se do repozitáře neukládá samostatně.
  • Každá template definition nese vlastní layout, včetně page size, TrimBoxu, dynamických oblastí, souřadnic QR a typografických parametrů. Renderer je obecný a pouze řeší templateKey -> registry -> master + layout; neobsahuje větvení pro konkrétní designy. DIGITAL a PRINT používají stejný layout, DIGITAL je pouze vektorový crop PRINT stránky. Nová šablona musí projít úplným checklistem výše včetně trusted asset readeru, registry, layoutu a regresních/preflight testů; formulář vytvoření i Nastavení → Vouchery ji pak načtou automaticky z aktivních definition. allowedTypes filtruje UI a je znovu vynuceno serverem.
  • Preview šablon je lokální asset z registry, nikoli Media Manager; chybějící PNG zobrazí v admin formuláři náhradní stav a neblokuje voucherový backend ani PDF generování.
  • Sdílený worker-safe core v src/features/vouchers/lib/voucher-pdf-core.ts pro classic-v1 personalizuje master PRINT stránku 216 × 105 mm s bleedem a TrimBoxem 210 × 99 mm na offsetu 3 mm. Tyto hodnoty jsou uloženy v layoutu classic-v1 a exportované konstanty v core jsou pouze zpětně kompatibilní aliasy. DIGITAL vzniká vektorovým výřezem stejné PRINT stránky; routy /pdf a /pdf/tisk zachovávají původní URL kontrakt.
  • PDF generátor používá pdf-lib, qrcode, @pdf-lib/fontkit a Noto Sans z @fontsource/noto-sans, aby bezpečně fungovala česká diakritika bez commitování fontů do repozitáře. QR kód míří na veřejnou URL /vouchery/overeni?code=... nad siteConfig.url.
  • SiteSettings.voucherPdfLogoMediaId zůstává v DB kvůli zpětné kompatibilitě, ale aktuální admin UI ani voucherový PDF/email generator ho nepoužívají. Grafika voucheru včetně loga a pevných kontaktních údajů je součástí verzovaného master PDF a PDF šablony se do Media Manageru nepřidávají.
  • Voucherový PDF renderer nečte kontaktní údaje ze SiteSettings ani z VOUCHER_PUBLIC_DOMAIN / NEXT_PUBLIC_SITE_DOMAIN: při načtení masteru ověří jeho rozměr podle registry a poté doplní pouze hodnotu nebo název služby, platnost, kód voucheru a QR kód. Pevná grafika, logo, adresa, web a ostatní texty jsou součástí masteru. Kontakty pro web a e-maily nadále používají SiteSettings.
  • Tvorba voucheru běží přes konkrétní routy /admin/vouchery/novy a /admin/provoz/vouchery/novy, route factory createAdminVoucherCreateRoute(...), sdílený formulář src/features/admin/components/admin-voucher-form.tsx a server action createAdminVoucherAction(...). Admin action vždy ověřuje roli OWNER nebo SALON, znovu validuje Zod schéma a pro SERVICE povolí jen aktivní službu. Formulář předvybere templateKey, validFrom nastaví na dnešek a validUntil počítá podle SiteSettings.voucherDefaultValidityMonths; při jediné aktivní šabloně zobrazí její preview bez zbytečného dropdownu. Prezentačně formulář v této verzi sbírá jen údaje o kupujícím; doménová pole recipientName a message zůstávají v backend kontraktu, ale admin UI je nepoužívá.
  • Ruční odeslání voucheru e-mailem je v detailu voucheru přes klientský panel src/features/admin/components/admin-voucher-email-panel.tsx a server action src/features/admin/actions/voucher-email-actions.ts.
  • Server action sendVoucherEmailAction(...) vždy ověřuje session i roli OWNER nebo SALON; samotný enqueue core je v queueVoucherEmailLog(...) a validuje voucherId, recipientEmail, subject přes Zod.
  • Odeslání je povolené jen pro efektivní stavy ACTIVE a PARTIALLY_REDEEMED (getEffectiveVoucherStatus(...)); DRAFT, REDEEMED, EXPIRED, CANCELLED vrací business chybu bez raw stack trace do UI.
  • Email outbox používá existující EmailLog s novým enum typem EmailLogType.VOUCHER_SENT a template key voucher-sent-v1. V EMAIL_DELIVERY_MODE=background se záznam frontuje pro worker + retry; v log režimu se zapisuje jako odeslaný log bez SMTP.
  • Šablona voucher-sent-v1 je napojená v src/lib/email/templates.ts a používá server-side helper buildVoucherEmailTemplate(...) z src/features/vouchers/lib/voucher-email-template.ts. Příloha je DIGITAL PDF z worker-safe generateVoucherDigitalPdf(...) v src/features/vouchers/lib/voucher-pdf-core.ts (bez HTTP callu na admin route), filename voucher-KOD.pdf, content type application/pdf.
  • Obsah voucher e-mailu je v této verzi záměrně pevný (bez volného textarea vstupu v admin formuláři): pozdrav, věta v příloze zasíláme dárkový poukaz PP Studio., detail poukazu, ověřovací URL, závěr a kontakt salonu včetně domény.
  • Voucher e-mail nikdy nesmí obsahovat internalNote, historii čerpání, technická ID nebo booking metadata; posílá jen bezpečné business údaje + veřejný ověřovací odkaz /vouchery/overeni?code=....
  • Admin uplatnění voucheru u rezervace běží v detailu rezervace přes src/features/admin/components/admin-booking-voucher-form.tsx a server action redeemBookingVoucherAction(...) v src/features/admin/actions/booking-actions.ts. Action vždy ověřuje roli OWNER nebo SALON, volá výhradně doménové redeemVoucherForBooking(...), vrací bezpečné hlášky a revaliduje owner i salon booking/voucher přehledy. Jedna rezervace smí mít nejvýše jeden skutečně uplatněný voucher; další pokus se zastaví na doménové vrstvě.
  • Pokud detail rezervace už zobrazuje intended voucher z veřejného flow, formulář AdminBookingVoucherForm se renderuje přímo uvnitř stejné voucher karty. Nepoužívej klientský anchor skok na formulář níž v panelu; v praxi to působilo jako nečekané odskočení viewportu bez zjevné změny stavu.
  • Panel Úhrada v admin detailu rezervace kombinuje individuální cenu rezervace, settlement summary, voucherové čerpání a běžné platby mimo voucher. getAdminBookingDetailData(...) vrací voucher.paymentSummary: totalPriceCzk bere z efektivní ceny Booking.finalPriceCzk ?? Booking.servicePriceFromCzk, fallbackově z aktuální Service.priceFromCzk; voucherPaidCzk je součet VoucherRedemption.amountCzk; directPaidCzk je součet BookingPayment.amountCzk; paidAmountCzk / paidTotalCzk počítá helper getBookingPaymentSummary(...); remainingAmountCzk je doplatek po voucheru i běžných platbách. Booking.finalPriceCzk je provozní obchodní úprava ceny se zdůvodněním pro OWNER i SALON, ne záporná platba. Pro VALUE voucher se doporučená částka odvozuje z doplatku po finální ceně; SERVICE voucher částku nezadává a doménově řeší jen shodu služby.
  • Vizuální priorita panelu Další krok je aktuální stav -> hlavní provozní CTA -> sekundární provozní akce -> nebezpečná akce. U CONFIRMED rezervace musí být Dokončit návštěvu nejvýraznější akce, Přesunout termín a Nedorazila sekundární a Zrušit rezervaci oddělené v samostatné danger sekci s důvodem zrušení. Nemíchej storno do běžného chooseru vedle dokončení návštěvy.
  • Aktuální completion flow používá CTA Dokončit návštěvu. Pokud je doplatek > 0, akce má vést přes kompaktní volbu režimu úhrady (Hotově, QR, Voucher, Kombinovaně, Bez platby) a teprve potom uzavřít status na COMPLETED.
  • Bez platby nesmí být tichá zkratka: v completion flow vyžaduje povinný důvod a do historie se zapisuje explicitní informace, že rezervace byla dokončená s neuhrazeným doplatkem.
  • Server action completion flow je completeBookingVisitAction(...) v src/features/admin/actions/booking-actions.ts; při completion může zapsat BookingPayment, uplatnit voucher přes existující doménu redeemVoucherForBooking(...) a následně provést status transition přes applyAdminBookingStatusChange(...).
  • Před zápisem platby/voucheru musí completion flow znovu ověřit canCompleteBookingAt(...) a plánovanou úhradu proti aktuálnímu doplatku. Pokud hotovost/QR/voucher/kombinace nepokryje celý doplatek, action vrátí chybu a admin musí doplnit úhradu nebo vědomě použít Bez platby s důvodem.
  • Completion panel pro režim Voucher a Kombinovaně má pomocné načtení voucheru přes API POST /api/admin/vouchers/lookup; po zadání kódu vrací stav voucheru a u VALUE předvyplní doporučenou částku podle zůstatku voucheru a aktuálního doplatku.
  • Klientský lookup ve completion panelu musí být odolný proti race condition: starší requesty se abortují a stale odpovědi se ignorují, aby úspěšně načtený voucher nepřepsala pozdější chybová hláška.
  • Lookup request ve completion panelu používá credentials: same-origin a má jeden automatický retry při síťové chybě; fail větev loguje chybu do konzole s kódem voucheru a request id pro snadnější provozní diagnostiku.
  • Lookup endpoint přijímá výhradně POST /api/admin/vouchers/lookup s kódem voucheru v JSON body; completion panel používá cache: no-store a endpoint vrací Cache-Control: private, no-store.
  • Pro kompaktní provozní variantu detailu drž panel Další krok nízký: krátký status řádek bez duplicit, kompaktní akční tlačítka v jedné mřížce a potvrzení vybrané akce jako jeden řádek (vysvětlení + důvod + potvrdit) bez velké opakující se preview karty.
  • Sekundární karta Přesunout termín v panelu Další krok musí být shrink-safe: wrapper grid položky i success banner po uložení mají dovolit min-w-0 a zalamování textu, aby dlouhé labely termínu nebo warning copy nerozbily layout completion flow.
  • Sekce Nebezpečné akce má být výchozně sbalená (Nebezpečné akce + Rozbalit), aby Zrušit rezervaci nebyla dominantní při běžném otevření detailu.
  • Vizuální priorita panelu Úhrada je záměrně Stav úhrady -> Cena k úhradě -> doplatek -> + Zapsat platbu -> + Uplatnit voucher -> Přehled úhrad. Horní PaymentSummaryBlock je dominantní a obsahuje i kompaktní vstup do individuální úpravy ceny přes Upravit u položky Cena k úhradě; samostatný viditelný BookingPriceBlock se ve výchozím zobrazení nepoužívá. Doplatek nebo přeplatek má být nejsilnější finanční hodnota. + Zapsat platbu drž dobře viditelné, ale ne silnější než hlavní CTA v panelu Další krok; existující běžné platby nevypisuj ve zvláštním bloku mimo ledger, patří jednou do Přehled úhrad spolu s voucher redemptions a případným smazáním platby. Voucher akci drž jako sekundární/outline přímo v kompaktním voucher bloku a prázdný Přehled úhrad vizuálně tlumený.
  • V kompaktním detailu může být platební ledger pod rozbalením Detail úhrady, ale výpočty a data (paidTotalCzk, voucherPaidCzk, directPaidCzk, remainingAmountCzk) se nesmí měnit.
  • Stav úhrady je jen odvozený read model, ne uložený DB stav: UNPAID, PARTIALLY_PAID, PAID nebo OVERPAID. BookingPayment je ledger přijatých plateb mimo voucher (CASH, CARD, BANK_TRANSFER, OTHER); voucherové čerpání dál zůstává výhradně ve VoucherRedemption. OWNER i SALON smí platbu zapsat, mazání platby je omezené na OWNER.
  • U hodnotového voucheru s nižším zůstatkem než cena služby admin formulář předvyplňuje maximální použitelnou částku a vysvětluje doplatek mimo voucher. Serverová doména je autoritativní: pokud zadaná částka převyšuje remainingValueCzk, uplatní pouze dostupný zůstatek voucheru. Server action po částečné úhradě vrací success hlášku s uplatněnou částkou a dopočteným doplatkem vůči zadané částce.
  • Read model getAdminBookingDetailData(...) smí pro sekci Dárkový poukaz v panelu Úhrada vracet jen provozně bezpečná pole: intended voucher kód, typ, efektivní stav, bezpečný popis, doporučenou částku a historii redemptionů s aktérem. Technická ID nemají být primárním UI údajem.
  • Detail voucheru smí zobrazovat interní poznámku pouze v adminu; veřejné PDF ani veřejné ověření voucheru ji nesmí číst z veřejného read modelu.
  • Veřejné ověření voucheru běží na /vouchery/overeni?code=..., má noindex metadata a záměrně není v sitemap.ts. Používá verifyVoucherPublic(...), smí zobrazit jen kód, typ, zbývající hodnotu u VALUE, název služby ze snapshotu u SERVICE a platnost do; pro CTA může interně předat i veřejný slug navázané služby, nikdy však technické ID ani citlivá data. Neplatným kódům vrací pouze bezpečné důvody. Následné CTA po platném výsledku může vést na rezervaci nebo veřejný kontakt studia, ale nesmí spouštět uplatnění ani zapisovat voucher data.
  • Veřejná prezentační route /vouchery je indexovatelná landing page pro akvizici a rozhodnutí před nákupem voucheru. Patří do sitemap.ts, používá oddělený metadata helper public-page-metadata.ts, vlastní komponentu voucher-landing-page.tsx, pro blok doporučených služeb má číst jen úzký výběr přes getVoucherSuggestedServices(3) místo celého katalogu a má vést jen na bezpečné veřejné kroky (kontakt, mailto:, tel:, /vouchery/overeni, případně detail služby), ne na admin nebo přímé čerpání voucheru.
  • Stejný princip drž i pro veřejné route /faq a /kontakt: pokud není potřeba sdílet celý page modul napříč webem, preferuj route-specific komponenty (faq-page.tsx, contact-page.tsx) a lehký metadata helper před importem celého public-site.tsx, aby cold compile v Next.js 16/Turbopack netahal zbytečný modulový strom.
  • Stejný princip drž i pro homepage / a katalog /sluzby: preferuj route-specific komponenty (public-home-page.tsx, services-page.tsx) a metadata helper před importem celého public-site.tsx, zvlášť když route potřebuje jen malou část veřejného UI a jinak by zbytečně natahovala další public stránky do stejného kompilovaného stromu.
  • Route /vouchery/overeni má server-side anti-bruteforce guard v src/features/vouchers/lib/voucher-public-verification-rate-limit.ts: IP hash přes ADMIN_SESSION_SECRET, okno 10 minut, limit 10 pokusů/IP a audit log do BookingSubmissionLog s prefixem PUBLIC_VOUCHER_VERIFY_*.
  • Route /vouchery/overeni je read-only: nesmí vytvářet VoucherRedemption, měnit remainingValueCzk, měnit Voucher.status, ukládat booking intent ani číst admin-only read model.
  • Veřejný submit /rezervace může přijmout volitelný voucherCode, ale nesmí odečítat zůstatek, měnit status voucheru ani vytvářet VoucherRedemption; smí pouze uložit intendedVoucherId, intendedVoucherCodeSnapshot a intendedVoucherValidatedAt na Booking.
  • VALUE voucher se ve veřejném flow považuje za použitelný při kladném remainingValueCzk bez ohledu na cenu služby; nižší zůstatek než cena není chyba a doplatek zůstává provozní záležitost při návštěvě.

Veřejný Web

  • Každá veřejná stránka má vlastní route a metadata.
  • Detail služby běží na sluzby/[slug] a čerpá z request-time DB read modelu, aby admin změny byly vidět bez rebuildů.
  • V Next.js 16 mají dynamické route props asynchronní API; v page.tsx a generateMetadata proto typuj params jako Promise<...> a čti je přes await params (jinak vzniká chyba sync-dynamic-apis).
  • Veřejný web drží dva zdroje obsahu:
    • marketingové bloky, FAQ a právní texty jsou dál centralizované v src/content/public-site.ts
    • služby a ceník berou data z DB přes src/features/public/lib/public-services.ts
  • Krátké storno shrnutí mimo samostatné právní stránky navrhuj jako prezentační microcopy, ne jako procesní nebo interní termínologii: pro homepage trust metriku a FAQ preferuj formulace orientované na akci klientky (upravit nebo zrušit termín, nejpozději 24 hodin předem) a drž je čitelné během 1-2 sekund.
  • FAQ už používá strukturovaný model FaqSection -> FaqItem; při dalších úpravách preferuj tematické skupiny a krátké odpovědi před jedním plochým seznamem dlouhých textů.
  • FAQ stránka renderuje odpovědi serverově přes nativní details/summary; nepřidávej klientskou state vrstvu, která by odpovědi vkládala do DOM až po kliknutí.
  • FAQPage JSON-LD staví buildFaqPageJsonLd(...) ze stejného seznamu sekcí jako stránka. Při úpravě FAQ nejdřív změň viditelný FaqItem a až z něj nech vzniknout strukturovaná data, aby se schema nikdy nerozjelo s obsahem stránky.
  • Praktické FAQ má pokrývat i opakující se salonní dotazy kolem frekvence kosmetiky, příchodu s make-upem, citlivé pleti, úpravy obočí a výdrže barvení obočí; drž odpovědi konkrétní, ale bez medicínských slibů.
  • FAQ průběžně rozšiřuj i o rozhodovací dotazy s organickým potenciálem: jak vybrat první službu, kdy zvolit lash lifting vs. laminaci obočí, jak dlouho tyto služby vydrží, kdy je lepší návštěvu kvůli podráždění očí odložit a kdy je vhodnější hodnotový voucher než voucher na konkrétní službu.
  • src/features/public/lib/public-services.ts nyní zároveň funguje jako thin read model nad rozšířeným katalogem:
    • Service nese publicIntro, seoDescription, pricingShortDescription, pricingBadge; název služby je sjednocený v Service.name
    • ServiceCategory nese pricingDescription, pricingLayout, pricingIconKey, pricingSortOrder; veřejný název kategorie jde z aktuálního ServiceCategory.name
    • fallbacky pořád existují, ale primární zdroj veřejné copy už je databáze, ne lokální slug mapy
  • Ceník na /cenik má vlastní skladbu v src/features/public/components/pricing-page.tsx; obecný public-site.tsx už neobsahuje pricing-specific layout logiku.
  • Pricing modul je rozdělený na komponenty PricingHero, CategoryChips, PricingSection, PricingItem, PricingGridSection a PricingCTA, aby šlo věrně ladit spacing a hierarchii bez zásahu do ostatních veřejných stránek.
  • /cenik už nečte prezentační metadata z lokálních map; route používá getPublicPricingCatalog() a dostává z DB hotové kategorie včetně badge, icon key a layoutu.
  • Pořadí kategorií na /cenik má být konzistentní s /sluzby a /rezervace: priorita je ServiceCategory.sortOrder, až potom pricingSortOrder.
  • Veřejné mapování kategorií je sdílené i pro booking katalog (src/features/booking/lib/booking-public/catalog.ts) přes stejné pole ServiceCategory.name, aby /rezervace používala stejný label jako /sluzby a /cenik.
  • Public pricing read model (getPublicPricingCatalog) validuje, že každá služba je v ceníku zařazená právě jednou kategorií; duplicita stejného slug přes více kategorií je tvrdá validační chyba.
  • Ceník už nepoužívá doprovodný blok s poznámkami.
  • Úvodní stránka používá stejný DB katalog pro featured služby, aby odkazy z homepage mířily na aktuální slugs. Ruční výběr řídí Service.isFeaturedOnHomepage a homepageSortOrder; public read model bere maximálně tři aktivní veřejné služby v aktivních kategoriích a při prázdném výběru padá zpět na katalogové pořadí.
  • Reusable page sekce jsou ve src/features/public/components/public-site.tsx.
  • FAQ layout v src/features/public/components/public-site.tsx je záměrně server-rendered bez další klientské state vrstvy; pro rozbalování odpovědí preferuje nativní details/summary, aby zůstal lehký a dobře kliknutelný i na mobilu.
  • Pravý box ve FAQ hero nemá opakovat kontaktní CTA z levé části; slouží jako klidný informační panel k první návštěvě s nanejvýš nenápadným anchor odkazem na #prvni-navsteva.
  • Rychlá orientace FAQ používá anchor odkazy na sekce, globální smooth scroll a mobilní tap targety alespoň kolem 44 px; při dalších úpravách drž odkazy dostatečně velké i při delších českých názvech.
  • U homepage hero drž jako preloadovaný LCP kandidát pouze logo (next/image preload); portrait nemá mít stejnou prioritu, aby nebral bandwidth/render budget prvním pixelům loga.
  • Kontaktní stránka má vlastní modulární sekce v src/features/public/components/contact-sections.tsx:
    • ContactHero
    • ContactMapPreviewCard
    • ContactParkingInfoCard
    • QuickContactCard
    • ContactCard
    • ContactCTA
    • ContactMobileStickyCTA
  • Kontakt data (buildContactItems) drží i provozní mikrocopy a Google Maps deep-link pro adresu; odkaz pro adresu má mířit na konkrétní firemní profil Kosmetika | Pavlína Pomykalová, zatímco iframe náhled může dál používat stabilní query podle adresy. Aktuální skladba stránky používá především kompaktní map preview, pravý quick contact panel a navazující full-width parkovací sekci.
  • ContactParkingInfoCard na /kontakt patří pod celou kontaktní mřížku, ne jen do levého sloupce. Aktuálně renderuje 4 nízké tip cards (Hradská, Gahurova, Sadová, Kongresové centrum Zlín) s orientační cenou pro návštěvu 90-120 minut, krátkou poznámkou a odkazem Navigovat.
  • U Kongresové centrum Zlín preferuj copy podle nejběžnějšího scénáře klientek: Po–Pá přes den. Pokud je karta určená hlavně pro pracovní dobu cca 8:00–18:00, komunikuj 70 Kč pro 90-120 minut jako běžnou orientaci a levnější večerní či víkendové režimy nech jen v drobném doplňku.
  • V parkovací sekci drž jen stručné doporučení podle ceny a pohodlí; nevracej se k dlouhé tabulce nebo plnému sazebníku. Další možnosti v centru (Městské divadlo, Nad Tržnicí, Zlaté Jablko) smí být zmíněné nanejvýš v jedné doplňkové větě, ne jako hlavní karty.
  • ContactHero má při přítomnosti fotky studia držet dvousloupcovou skladbu text vlevo / obraz vpravo, na mobilu přirozeně padá pod text a hero obrázek nesmí přetékat mimo panel. Pro Next.js 16 používej u above-the-fold kontakt hero obrázku loading="eager" a smysluplné sizes; nepřidávej zpět deprecated priority.
  • Stránka /o-mne už neběží jako jeden blok v public-site.tsx; vlastní skladba je v src/features/public/components/about-page.tsx.
  • Stránka /o-mne je rozdělená do sekcí HeroSection, WhyChooseMeSection, StorySection, ApproachSection, WhatToExpectSection a CertificationsSection, aby šlo pracovat s hierarchií bez monolitického JSX bloku.
  • Další vizuální ladění /o-mne preferuje jemný polish přímo v těchto sekcích místo dalšího přestavování IA; hlavní páky jsou proporce gridů, padding, typografická síla a optické vyvážení spodního okraje stránky.
  • U finálního polish passu preferuj drobné úpravy max-width, gap, line-height, hover a shadow před zásahy do obsahu nebo dalšího členění sekcí.
  • Pokud stránka O mně potřebuje další micro tuning, drž se jen utility tříd v existujících komponentách about-page.tsx a about-certificates-gallery.tsx.
  • aboutContent v src/content/public-site.ts používá strukturovaný model (profile, whyChooseMe, story, approach, expectations, cta), aby bylo možné copy i CTA upravovat bez zásahu do layoutu; whyChooseMe podporuje krátký podnadpis sekce přes description a položky whyChooseMe.items mohou mít vedle titulku i description pro vysvětlující benefit text.
  • Copy na /o-mne má zůstat klidné, dospělé a osobní: neupozorňuj defenzivně na délku praxe, nepoužívej přehnané sliby ani superlativy a preferuj konkrétní popis přístupu, průběhu péče a přirozeného výsledku.
  • Veřejná klientská copy má reflektovat, že salon provozuje jedna osoba. Při úpravách textů v src/content/public-site.ts, src/features/public/components/* a public route asides nepoužívej zbytečně týmové množné číslo typu doporučujeme tam, kde má mluvit provozovatelka; přirozené společné formulace s klientkou (společně doladíme) a studio jako místo (k nám) jsou v pořádku.
  • Certifikace na /o-mne berou public data z src/features/public/lib/public-certificates.ts, ale UI je záměrně připravené i na nulový stav pomocí placeholder karet v AboutCertificatesGallery.
  • Pro konzistentní vizuální rytmus napříč veřejnými stránkami drž hlavní obsah v jednotném wrapperu Container (max-w-7xl) a vyhýbej se dalším globálním zúžením přes mx-auto max-w-* na úrovni celé sekce.
  • Pro konzistentní spacing preferuj na veřejných stránkách vertikální rytmus py-10 / sm:py-14 / lg:py-16; větší rozestupy používej jen tam, kde mají jasný obsahový důvod (např. hero nebo výrazný CTA blok).
  • src/components/layout/site-footer.tsx má být kompaktní informační footer:
    • drž rozdělení na 3 desktop bloky brand -> navigace/informace -> kontakt
    • uprostřed vždy zachovej dvě oddělené skupiny odkazů Navigace a Informace
    • kontakt má mít vyšší vizuální váhu než běžné linky, ale bez CTA nebo promo copy; obsahuje adresu, telefon a e-mail
    • ve footeru zobrazuj e-mail i telefon v přirozeně čitelném tvaru; nepoužívej textový zápis info [at] ...
    • footer i header udržuj server-rendered všude, kde to jde; kvůli trackingu nepřepínej celý shell na klientský rendering
    • při dalších úpravách preferuj práci s gap, max-width, typografickou vahou a jemným border/shadow rytmem místo přidávání dalších obsahových bloků
  • Veřejné e-mailové odkazy nepoužívej jako surový mailto: přímo v SSR markupu; pro footer, kontaktní stránku a další veřejné kontaktní prvky používej src/components/ui/obfuscated-email-link.tsx, který:
    • má mít výchozí UI v plně čitelném tvaru s @; textový zápis local [at] domain používej jen když je to výslovně zamýšlené konkrétním UI copy
    • skládá skutečný mailto: až v klientu
    • umí i předvyplněný subject a body pro provozní e-mailové akce
  • Právní stránky ve src/features/public/components/public-site.tsx mohou nově používat rozšířenou skladbu hero + aside + anchor TOC + sekce; pro právní texty proto preferuj strukturovaný obsah v src/content/public-site.ts místo dlouhých monolitických odstavců.
  • Model LegalSection podporuje id, seznamové body a volitelnou poznámku. U GDPR a podobných stránek tím drž kratší odstavce, lepší scanovatelnost a jasné anchor odkazy.
  • Placeholder obsah musí být jasně odlišen od finálních produkčních textů.
  • Pokud je interní název služby příliš technický nebo exportovaný ze starého webu, uprav veřejné texty v adminu služby místo přidávání trvalého override v read modelu.
  • Pro katalogová i veřejná metadata preferuj přímo pole v katalogu (Service.publicIntro, Service.description, Service.pricingShortDescription, Service.seoTitle, Service.seoDescription, Service.idealFor, Service.includes, Service.benefits, Service.goodToKnow, ServiceCategory.pricingDescription).
  • src/features/public/lib/service-copy-overrides.ts je pouze dočasný migrační/backfill zdroj podle slug; veřejný web ho nemá používat jako hlavní ani prioritní zdroj obsahu.
  • CTA na rezervaci držet konzistentně v headeru, hero sekcích a kontextových blocích.
  • U homepage copy preferovat strukturu, která se už historicky osvědčila: jasný lokální hero claim, dvě primární akce (rezervace + ceník) a blok „nejste si jistá výběrem“, který snižuje bariéru první rezervace.
  • Pokud homepage potřebuje logo/fotku majitelky, nastav to v homepageContent (logoImage, portraitImage) a používej lokální soubory z public/brand, aby nebyla závislost na externím hostingu.
  • Stránka /o-mne je výjimka z obecného pravidla „bez portrait-first kompozice“: může používat výraznější dvousloupcový hero s portrétem majitelky, pokud to pomáhá důvěře a rychlejšímu rozhodnutí klientky.
  • Statické soubory v public/brand jsou vhodné jen pro ručně verzované assety projektu; admin uploady mají používat sdílenou media vrstvu a model MediaAsset.
  • PWA manifest ikony odkazované v src/app/manifest.webmanifest přes root URL (např. /android-chrome-192x192.png) musí fyzicky existovat v public/; běžný soubor src/app/android-chrome-*.png bez file-convention názvu metadata route není automaticky servírovaný na stejné URL.
  • Pokud se mění brand logo pro browser chrome, regeneruj společně src/app/favicon.ico, src/app/apple-icon.png i public/apple-touch-icon*.png / public/android-chrome-*.png, aby zůstaly sladěné metadata route i manifest assets.

Auth Strategie

  • Login probíhá přes src/app/api/auth/login/route.ts.
  • Validace přihlašovacích údajů je server-side přes Zod.
  • POST /api/auth/login má server-side rate limit (10 minut, IP + e-mail hash) přes helper src/lib/auth/admin-login-rate-limit.ts.
  • Audit login pokusů (SUCCESS, INVALID_PAYLOAD, INVALID_CREDENTIALS, RATE_LIMITED) se zapisuje do BookingSubmissionLog s prefixem ADMIN_LOGIN_*.
  • Session payload je podepsaný JWT token v httpOnly cookie.
  • Admin session cookie ppstudio-admin-session má idle expiraci 14 dní a používá sliding refresh v src/proxy.ts: pokud do expiry zbývá méně než 48 hodin, proxy vystaví novou cookie/JWT se stejným payloadem.
  • Session současně respektuje absolutní limit 45 dní od prvního přihlášení (sessionStartedAt claim); po překročení proxy cookie smaže a přesměruje na /admin/prihlaseni.
  • Hodnoty jsou konfigurovatelné přes env: ADMIN_SESSION_IDLE_MAX_AGE_SECONDS, ADMIN_SESSION_REFRESH_WINDOW_SECONDS, ADMIN_SESSION_ABSOLUTE_MAX_AGE_SECONDS (sekundy). Guardrails: refresh window nesmí být větší než idle timeout a absolute timeout nesmí být menší než idle timeout.
  • src/proxy.ts řeší rychlý auth gate pro admin: u /admin/* ověřuje podpis a expiraci session JWT cookie, ne jen její existenci; neplatnou cookie smaže a přesměruje na login.
  • Role-based autorizace se dokončuje uvnitř serverových helperů v src/lib/auth/session.ts; po ověření JWT se admin session znovu načítá z DB, neaktivní uživatel session zneplatní a aktuální role se bere z DB.
  • Admin login přijímá výhradně aktivní databázové účty s passwordHash. Recovery CLI obnoví DB OWNERa, zapíše auditní záznam a revokuje otevřené pozvánky; heslo se nepředává jako argument příkazu.
  • Owner approve/reject odkazy z e-mailu už smějí změnit stav rezervace pouze po aktivní admin session. Tokenová stránka může zobrazit akci, ale server action před mutací znovu volá admin auth helper.
  • Pro role-based admin IA používáme dvě serverově chráněné oblasti:
    • OWNER na /admin/*
    • SALON na /admin/provoz/*
  • Neplatná nebo zakázaná admin sekce se neřeší jen skrytím v menu; routa se validuje server-side přes src/features/admin/lib/admin-guards.ts.
  • Sekce Přístupy má vlastní owner-only route workflow v src/features/admin/components/admin-users-page.tsx; už nepoužívá generický placeholder renderer z admin-section-page.tsx.

Testovací Strategie

  • Unit testy běží přes Node test runner (npm run test:unit); plný lokální preflight (npm test) na ně navazuje všemi DB integracemi.
  • Všechny DB integrační scénáře lze spustit cíleně a sériově přes npm run test:db:integration; pouze booking podmnožinu přes npm run test:db:booking.
  • Playwright E2E sada (npm run test:e2e) má dvě vrstvy:
    • booking-flows.spec.ts ověřuje kritické rezervační a provozní workflow nad reálnými fixture daty.
    • voucher-flows.spec.ts ověřuje provozní lifecycle hodnotového voucheru přes browser: vytvoření v adminu, chráněné stažení PDF a otevření předvyplněného panelu pro ruční odeslání e-mailu. Samotné zařazení e-mail logu kryje integrační test voucher-email-actions.integration.test.ts.
    • site-smoke.spec.ts ověřuje základní dostupnost veřejných rout, sitemap/robots kontrakt, bezpečné chybové stavy veřejných utility rout, auth redirect protected adminu a načtení hlavních OWNER/SALON sekcí bez plošného klikání každé akce.
    • accessibility.spec.ts spouští axe nad reprezentativním veřejným webem, formulářem rezervace a přihlášeným admin dashboardem.
  • Regrese ranního planner problému je krytá integračním testem admin-slots/mutations.integration.test.ts: publikace konceptu přes den s existující rezervací musí rezervovaný interval zachovat a přepsat jen běžnou dostupnost před/po něm.
  • Browser E2E testy běží přes Playwright (npm run test:e2e) v adresáři tests/e2e.
  • Playwright spouští suite v projektech chromium, mobile-chrome (emulace Pixelu 5) a mobile-safari (WebKit, emulace iPhonu 15), aby stejné kritické scénáře zachytily desktopové i mobilní regrese.
  • Na Node 24 nepoužívej Playwright 1.59.1: v GitHub Actions se tento release uměl zaseknout při npx playwright install --with-deps chromium po stažení Chrome for Testing. Drž minimálně 1.60.0+; repozitář je aktualizovaný na ^1.62.1.
  • CI po samostatném npm run build spouští E2E přímo přes npx playwright test, aby se kvůli pretest:e2e neprováděl druhý identický build.
  • pretest:e2e pro Playwright build musí používat stejný build-time origin jako následný next start: nastavuje NEXT_PUBLIC_APP_URL=${PLAYWRIGHT_BASE_URL:-http://127.0.0.1:3100} a zároveň dummy analytics env pro Matomo a Meta Pixel (NEXT_PUBLIC_MATOMO_ENABLED=true, NEXT_PUBLIC_MATOMO_URL=https://matomo.example.test/, NEXT_PUBLIC_MATOMO_SITE_ID=1, NEXT_PUBLIC_META_PIXEL_ENABLED=true, NEXT_PUBLIC_META_PIXEL_ID=123456789). Jinak admin login/logout redirecty a proxy ochrana v produkčním buildu zůstanou inlinované na jiný host a browser testy po přihlášení skončí cross-origin ERR_CONNECTION_REFUSED; Matomo smoke testy zase neuvidí žádné _paq volání, protože tracking byl vypnutý už při buildu.
  • Stejné dummy analytics env musí mít i každý samostatný CI next build krok. NEXT_PUBLIC_* hodnoty se inlinují už při buildu, takže pouhé runtime env v playwright.config.ts nestačí, pokud test běží nad předem sestaveným .next.
  • Reschedule scenar client can reschedule a booking through a public token ma zamerne sirsi test timeout nez ostatni scenare, protoze overuje plny self-service submit a success render nad produkcnim next start serverem.
  • Pri finalnim cekani na success heading je timeout zamerne navyseny na 30_000 ms; pri nezdaru test navic vypise posledni viditelnou chybu formulare, aby CI log hned ukazal, jestli slo o konflikt slotu, validaci nebo obecny save error.
  • Admin smoke scenare owner can open the core backoffice sections a salon role can open the operational workspace but not owner-only sections mají explicitní timeout 90_000 ms, protože sekvenční průchod více backoffice rout v CI běžně přesahuje výchozích 45_000 ms.
  • Playwright konfigurace používá lokální produkční next start server na PLAYWRIGHT_PORT (výchozí 3100) a nastavuje NEXT_PUBLIC_APP_URL na stejný lokální origin pro runtime serveru.
  • E2E runtime ukládá SITE_SETTINGS_SNAPSHOT_PATH do /tmp, aby testovací server nezapisoval do produkční cesty /var/lib/ppstudio a CI nelogovalo chybu oprávnění.
  • Protože NEXT_PUBLIC_APP_URL se inlinuje už při next build, musí stejnou hodnotu dostat i build krok. CI i lokální pretest:e2e proto buildí s NEXT_PUBLIC_APP_URL=http://127.0.0.1:3100 (nebo PLAYWRIGHT_BASE_URL) a současně drží NEXT_PUBLIC_SITE_URL=https://ppstudio.cz, aby veřejné canonical/JSON-LD URL zůstaly produkční.
  • Playwright runtime pro smoke coverage Matoma a Meta Pixelu navíc nastavuje dummy NEXT_PUBLIC_MATOMO_* a NEXT_PUBLIC_META_PIXEL_* hodnoty, aby server komponenty při next start skutečně renderovaly analytics trackery i v testovacím prostředí.
  • E2E fixture helper seeduje unikátní služby, sloty, klienty, tokeny a dočasného owner uživatele přes Prisma; cleanup filtruje podle unikátního runId, aby testy nesahaly na ručně vytvořená data.
  • U admin voucher pickeru nevaž Playwright locator na výchozí aria-pressed="false" stav konkrétní služby. E2E fixture služby používají velmi nízký sortOrder, takže testovaná služba může být v CI už předvybraná jako první aktivní položka; bezpečný test kliká obecný button[aria-pressed] podle názvu služby a teprve potom ověří aria-pressed="true".
  • Aktuální E2E smoke coverage ověřuje veřejné vytvoření pending rezervace, self-service storno, self-service přesun a owner potvrzení rezervace v admin detailu.
  • tests/e2e/booking-flows.spec.ts obsahuje i cleanup regrese pro veřejné rezervace:
    • seeduje službu s cleanupMinutes=10, vloží blokující potvrzenou rezervaci se snapshotem cleanupBlockMinutes=15 a ověří, že veřejný výběr nenabídne start na serviceEnd, ale nabídne první start až na blockedUntil
    • zvláštní scénář s jediným hodinovým slotem ověřuje, že poslední klientský start v okně zůstane rezervovatelný i tehdy, když cleanup přeteče za konec slotu
    • oba scénáře zároveň hlídají, že souhrn zůstává klientský (konec služby bez interního textu o úklidu)
  • U self-service přesunu (tests/e2e/booking-flows.spec.ts) je kolizní krok záměrně deterministický: test nejdřív vybere konkrétní UI slot, seedne k němu runtime konflikt v DB a až potom submituje změnu, aby vždy ověřil chybovou hlášku před úspěšným přesunem na jiný čas.
  • Kolizní a úspěšný náhradní čas v E2E fixture drž jako samostatné navazující published sloty se stejným veřejným popisem. Runtime konflikt pak blokuje jen zvolený hodinový segment a následný úspěšný submit není závislý na kapacitní validaci uvnitř jednoho dlouhého slotu.
  • Po konfliktní odpovědi u self-service přesunu už je potvrzovací tlačítko povolené z předchozí volby; test proto při výběru náhradního slotu musí čekat na aria-pressed=true a shodu hidden slotId i newStartAt, jinak může rychlý CI běh odeslat starý kolidující termín nebo nový čas se starým slotem.
  • Náhradní slot u self-service přesunu nevybírej přes nth(...) ani přes "další dostupné tlačítko". Fixture musí exportovat explicitní accessibility labely nebo ISO start časy pro konfliktní i úspěšný termín, aby test nebyl citlivý na změny 30min generovaných startů ani na pořadí renderu.
  • BookingManagementPanel pro veřejný self-service přesun používá u cílových sekcí scroll-mt-* a v selectDate / selectSlot okamžitý scrollBookingManagementTargetIntoView(...), který měří skutečnou .site-header--booking výšku a scrolluje přes window.scrollTo({ behavior: "auto" }). Nepřepínej tyto programové skoky zpět na smooth scroll bez ověření Playwrightem, protože rychlé klikání na vzdálenější sloty může proběhnout během animace a prvek pak není stabilní ve viewportu.
  • Pokud explicitní reschedule slot není mezi prvními navrženými dny, E2E helper selectSlotById(...) nejdřív z accessibility labelu vytáhne datum a klikne na kalendářové tlačítko Vybrat den ...; až potom hledá přesný čas. Díky tomu test nepadá na obecném nth(...) fallbacku jen proto, že vzdálenější slot ještě není vyrenderovaný v sekci vybraného dne.
  • E-mailový worker předává EmailLog.processingToken až do doručení i zápisu výsledku; žádný nový call deliverEmailLog nesmí token obcházet. Mimo worker (např. ruční „vytvořit a odeslat“) se job nejdřív atomicky claimuje přes claimEmailLogForImmediateDelivery(...). Resend REST dostává stabilní Idempotency-Key email-log/<EmailLog.id>; pro obecné SMTP je stabilní Message-ID pouze pomůcka pro deduplikaci, ne garance exactly-once.
  • Při paralelním CI běhu může i fixture „úspěšný“ slot po runtime kolizi zůstat obsazený jinou aktivní rezervací z jiného workeru. Reschedule E2E scénář proto po prvním neúspěšném submitu fallbackově zkouší další dostupné sloty, dokud neuvidí success heading nebo diagnostickou chybu formuláře.
  • Fallback helper ve stejném scénáři nesmí blokovat na locator('input[name=\"slotId\"]').inputValue(): po kliknutí na kandidátní slot nejdřív ověř rychlý success stav a při chybějícím/odpojeném hidden inputu pokračuj dalším kandidátem, jinak CI skončí na globálním timeoutu testu bez doménové regrese.
  • Veřejný booking rate-limit sdílí tabulku BookingSubmissionLog s admin loginem a veřejným ověřením voucheru, ale při počítání pokusů musí ignorovat prefixy ADMIN_LOGIN_ a PUBLIC_VOUCHER_VERIFY_. E2E fixture před tvorbou nových dat čistí krátké auditní okno, aby opakované lokální běhy nespouštěly rate-limit napříč nesouvisejícími scénáři.
  • U veřejného booking flow v E2E testech preferuj getByRole("textbox", { name: "E-mail" }) / getByRole("textbox", { name: "Telefon" }) před regexy, které předpokládají, že nápověda je součástí accessible name. Pokud test zakládá vlastní službu bez preselectu slugem, nejdřív otevři její fixture kategorii; stránka může být zároveň seedovaná jinými E2E kategoriemi.
  • DB integrační testy booking-public-voucher.integration.test.ts jsou seedované per test case (withSeed(...)) místo sdíleného global seedu, aby paralelní běh netvořil write konflikty přes společné service/slot/voucher zázemí.
  • Při úpravách Playwright locatorů preferuj stabilní business orientační body (sekce, heading, finální CTA) před přesnými dynamickými timestamp labely nebo historickými texty formulářů; admin detail rezervace aktuálně používá pole Volitelný důvod a submit text odpovídá zvolené akci, např. Potvrdit rezervaci.
  • Booking detail v adminu používá statickou hlavičku (bez sticky/plovoucího chování), aby nepřekrývala Další krok ani status chooser.
  • CI workflow .github/workflows/ci.yml běží na push do main/master, na pull requesty a lze jej ručně spustit přes GitHub Actions. Hlavní kontroly jsou samostatné joby lint, typecheck, test, build, e2e, e2e chromium shard 2, e2e mobile (mobile-chrome), e2e mobile (mobile-safari), e2e mobile shard 2 (mobile-chrome) a e2e mobile shard 2 (mobile-safari), takže GitHub UI i branch protection vidí každý check zvlášť. Job test po PostgreSQL service containeru, prisma migrate deploy a generování klienta spustí npm run test:ci: coverage unit vrstvy a následně sériové DB integrace; coverage report je jeho artefakt. Šest E2E jobů každý připraví vlastní PostgreSQL a build a spustí jeden shard Playwrightu: Chromium 1/2, mobile Chrome 1/2 a mobile Safari 1/2; mobilní Chrome instaluje Chromium a mobilní Safari WebKit. Job lint navíc bez PostgreSQL spouští npm run test:release-script.
  • Po rozdělení do samostatných jobů už mezi nimi nesdílíme pracovní adresář ani .next. typecheck proto po npm ci explicitně pouští npm run db:generate, build má vlastní PostgreSQL service + prisma migrate deploy kvůli route/page datům čteným při buildu a e2e si dělá vlastní build ještě před Playwright startem.
  • CI používá testovací env hodnoty přímo ve workflow a e-maily drží v EMAIL_DELIVERY_MODE=log, aby browser a DB testy nevytvářely reálné SMTP side effects.
  • Repo-level enforcement zůstává mimo git: po přidání nebo přejmenování workflow ručně slad branch protection / rulesets a required status checks v GitHub nastavení repozitáře, jinak budou nové kontroly jen informativní. Required checks hlavního CI mají mířit na přesné GitHub job names lint, typecheck, test, build, e2e, e2e chromium shard 2, e2e mobile (mobile-chrome), e2e mobile (mobile-safari), e2e mobile shard 2 (mobile-chrome) a e2e mobile shard 2 (mobile-safari); coverage sem nepatří, protože samostatný status check nevytváří.

Admin Informační Architektura

  • Sekce volne-terminy je znovu aktivní jako týdenní planner nad 30min gridem.
  • Serverový read/persistence model je v src/features/admin/lib/admin-slots.ts.
  • Server action adaptéry planneru jsou v src/features/admin/actions/slot-planner-actions.ts.
  • Jediným produkčním UI je FullCalendar: serverový wrapper AdminWeeklyPlannerPage používá klientský kalendář AdminWeeklyPlannerClient pro OWNER i SALON. Soubory si kvůli kontinuitě historie zatím ponechávají původní název *-lab-*; nejde o samostatný experimentální workflow ani veřejnou route.
  • src/components/layout/admin-shell.tsx je klientský kvůli mobilnímu draweru sidebaru; desktop shell zůstává záměrně úzký, aby většina šířky patřila planner gridu.
  • Pravý panel planneru je akční inspektor dne; na menších breakpointech se otevírá jako MobileInspectorSheet.
  • src/config/navigation.ts drží centrální definici admin sekcí, slugů a navigace pro obě role.
  • src/features/admin/components/admin-sidebar-nav.tsx je klientská navigace s aktivním stavem podle pathname.
  • Sdílené admin sekce pro OWNER i SALON zahrnují také vouchery; pokud se přidává detail, tvorba nebo čerpání voucheru, zachovej paralelní URL tvar /admin/vouchery/* a /admin/provoz/vouchery/*, pokud role nemá být záměrně omezená.
  • src/features/admin/components/admin-overview-page.tsx je po redesignu jen tenký server wrapper; skutečný overview workspace skládá src/features/admin/components/admin-dashboard-page.tsx.
  • src/features/admin/lib/admin-dashboard.ts drží serverový read model pro operativní dashboard dne:
    • hero Dnes
    • alerty
    • timeline rezervací + volných oken
    • spodní KPI
    • pravý sidebar stats / pending / upcoming slots / quick actions
  • admin-section-page.tsx dál obsluhuje generické nebo sekundární sekce; overview už na něj nenavazuje.
  • Druhé kolo visual polish overview je čistě prezentační: neměň read model ani sekce, pokud ladíš jen proporce, spacing, typografii nebo card rhythm.
  • Pro mobilní breakpointy overview dashboardu preferuj single-column stack před zmenšováním desktop kompozice: CTA pod sebe, alert CTA na vlastní řádek a quick actions až od sm do dvou sloupců.
  • src/features/admin/components/admin-booking-detail-page.tsx skládá detail rezervace jako decision-first layout:
    • statická kompaktní hlavička s klientka -> služba -> termín -> stav/zdroj -> rychlé akce
    • horní akční panel jako hlavní rozhodovací centrum
    • levý sloupec akce -> poznámky -> historie
    • pravý sloupec souhrn -> technická metadata
    • na mobilu stejné pořadí bez bočního sloupce
  • Při dalších úpravách detailu rezervace drž vizuální prioritu termín + klientka + stav -> další akce -> souhrn -> poznámky -> historie; nenechávej zpět narůst dlouhé vysvětlující texty ani duplicity mezi headerem a souhrnem.
  • src/features/admin/components/admin-booking-status-form.tsx zůstává malou klientskou vrstvou jen pro action chooser a submit server action; reschedule zůstává oddělený v RescheduleBookingButton a při dalších úpravách nenechávej do chooseru vracet slotový nebo drawer flow.
  • src/features/admin/components/admin-booking-note-form.tsx je oddělená klientská vrstva jen pro samostatnou editaci interní poznámky rezervace; drž ji bez dalších provozních rozhodnutí nebo statusové logiky.
  • Client.email je nově volitelný kvůli ručním rezervacím z Instagramu, telefonu a offline domluvy. Veřejný booking si vlastní povinný e-mail validuje zvlášť, ale admin tok i navazující CRM read modely musí bezpečně počítat s null a používat fallback Bez e-mailu nebo podmíněné mailto: odkazy.
  • src/features/admin/lib/admin-users.ts je serverový read model pro owner-only správu přístupů; skládá dohromady DB účty a systémové přístupy z bootstrap env vrstvy.
  • Pro klientsky bezpečné badge a texty kolem rolí/stavů používej src/features/admin/lib/admin-user-presentation.ts; nesahej pro runtime helpery do serverového read modelu, jinak se do klienta natáhne Prisma nebo session vrstva.
  • AdminUser má volitelné pole invitedAt, které drží čitelný stav Pozvánka čeká v owner UI.
  • Databázové přístupy používají heslo uložené v AdminUser.passwordHash; hash/verify helper je v src/lib/auth/password.ts.
  • Pozvánky používají model AdminUserInviteToken (hash tokenu, expirace, použití, revokace) a route /admin/pozvanka/[token].
  • Server actions pro owner správu přístupů jsou v src/features/admin/actions/admin-user-actions.ts:
    • saveAdminUserAccessAction pro založení pozvánky nebo úpravu jména/e-mailu; při nové pozvánce zároveň odesílá invite e-mail
    • changeAdminUserRoleAction pro jednoduché přepnutí mezi OWNER a SALON
    • setAdminUserActiveAction pro deaktivaci / opětovnou aktivaci
    • resendAdminUserInviteAction zůstává server action vrstva pro sdílenou logiku, ale UI resend v řádku uživatele je záměrně napojené přes API route src/app/api/admin/users/resend-invite/route.ts, aby bylo spolehlivé i v klientském list row workflow
  • Invite e-mail je záměrně posílaný přímo ze server action přes sendEmail, aby owner viděl výsledek hned po kliknutí bez čekání na background worker.
  • Dokončení pozvánky řeší activateAdminInviteAction a klientská komponenta AdminInviteActivationForm; po úspěchu login stránka zobrazuje informační stav invite=activated.
  • Aktivace pozvánky je bezpečnostně kritická transakce: consumeAdminInviteToken(...) zamkne řádek tokenu i AdminUser, vyžaduje usedAt IS NULL, revokedAt IS NULL, neprošlou expiraci a user.isActive = true, a teprve pak atomicky označí token jako použitý a uloží heslo. Nesmí nastavovat isActive. Deaktivace přes deactivateAdminUserAndRevokeInviteTokens(...) v tomtéž commitu nastaví účet na neaktivní a revokuje všechny nepoužité tokeny.
  • Regresi ověřuj databázovým testem src/features/admin/actions/admin-invite-activation.integration.test.ts: scénář deaktivace se starou pozvánkou musí vrátit chybu a ponechat účet bez hesla i neaktivní; dvě souběžné aktivace stejného tokenu smějí mít právě jeden úspěch.
  • UI sekce Přístupy je rozdělené do menších komponent AdminUsersWorkspace, UsersList, UserRow, InviteUserDialog, RoleCards, RoleBadge a AccountStatusBadge.
  • Systémové účty se v UI vědomě nepopsují jako bootstrap nebo env účty; tenhle slovník zůstává jen v technické dokumentaci a implementaci auth vrstvy.
  • Sekce Rezervace má vlastní workflow v src/features/admin/components/admin-bookings-page.tsx a už neběží přes generický placeholder renderer.
  • src/features/admin/lib/admin-data.ts pro rezervace nově vrací URL-driven read model s filtry, klikacími statistikami, seskupenými bloky seznamu a explicitními kontaktními odkazy místo obecného title/meta/description.
  • Validace search parametrů pro pracovní přehled rezervací je v src/features/admin/lib/admin-booking-list-validation.ts; drží hodnoty pro status, source a klikací stat. source je kanál rezervace (WEB, PHONE, INSTAGRAM, IN_PERSON, OTHER), zatímco marketingový původ z UTM/referreru patří do acquisition* polí.
  • src/features/admin/components/admin-bookings-toolbar.tsx používá next/form nad stejnou route a má zůstat nízký a kompaktní i na desktopu.
  • src/features/admin/components/admin-bookings-workspace.tsx je klientská vrstva pro click-to-open řádky, keyboard navigaci, selection shell a lehký toast feedback; booking business logika zůstává na server action vrstvě.
  • src/features/admin/components/admin-bookings-quick-actions.tsx je záměrně velmi malá klientská vrstva pro inline akce podle stavu rezervace; složitější workflow dál patří do detailu rezervace.
  • Na mobilu drž admin-bookings-page.tsx jako samostatný compact card pattern s pořadím čas -> klientka -> služba -> stav, ne jako smrštěnou desktop tabulku.
  • Horní statistiky sekce Rezervace jsou záměrně kompaktní segmented filter v jedné řadě; nepřidávej do nich další CTA ani sekundární texty typu Filtrovat.
  • Seskupení pracovního seznamu drž jen čtyři bloky Dnes, Zítra, Později, Dříve; Dnes má mít nejsilnější vizuální prioritu.
  • Sticky header rezervačního seznamu je součást provozního UX; při změnách shell layoutu ověř, že zůstane čitelný i při delším scrollu.
  • Sekce Služby má vlastní workflow v src/features/admin/components/admin-services-page.tsx a už neběží přes generický placeholder renderer.
  • src/features/admin/lib/admin-services.ts drží serverový read model pro seznam, provozní warningy, detail služby a předvyplněný create flow.
  • src/features/admin/actions/service-actions.ts nově obsluhuje create, update, duplikaci, quick toggles a reorder; validace zůstává v src/features/admin/lib/admin-service-validation.ts.
  • Editace služby při skutečné změně priceFromCzk zapisuje audit do ServicePriceChangeLog; aktér se mapuje z admin session e-mailu na reálné AdminUser.id, stejně jako u jiných provozních mutací.
  • Service.cleanupMinutes je interní provozní metadata služby s validací nezáporných celých minut; při vytvoření/přesunu rezervace se snapshotuje do Booking.cleanupMinutes a Booking.cleanupBlockMinutes (zaokrouhlení nahoru na 15 minut).
  • Booking dostupnost a kolize používají interní interval scheduledStartsAt -> blockedUntil, ale klientský termín i veřejné texty zůstávají scheduledStartsAt -> scheduledEndsAt bez zmínky o úklidu.
  • Při výběru termínu musí publikované okno pokrýt jen samotnou službu; cleanup blokace může přetéct za konec slotu. Backend kolize ale dál musí používat blockedUntil, aby se navazující slot nebo dodatečně publikované okno nenabídly dřív, než interní blokace opravdu skončí.
  • Totéž pravidlo drž i planner read model v src/features/admin/lib/admin-slots/queries.ts: availableIntervals se musí odečítat proti všem booking blokacím překrývajícím slot, ne jen proti bookingům se stejným slotId, jinak inspektor dne ukáže falešné Volné okno po cleanup overflowu do sousedního slotu.
  • Totéž pravidlo drž i admin dashboard read model v src/features/admin/lib/admin-dashboard.ts: sekce Nejbližší volné termíny nesmí zobrazit začátek slotu jen proto, že slot.bookings.length < capacity; musí odečíst všechny blokace scheduledStartsAt -> blockedUntil překrývající publikované sloty a navazující volné intervaly sloučit do stejných souvislých oken, jaká vidí planner.
  • Po zrušení rezervace přes klientský token i přes admin změnu stavu systém nově spouští post-cancel kompaktaci slotů: pokud se kolem zrušeného bookingu dotýkají sousední běžné publikované editable fragmenty a žádný z nich už nemá aktivní rezervaci, helper src/features/booking/lib/booking-slot-compaction.ts je sloučí zpět do jednoho souvislého slotu a případné historické CANCELLED booking vazby přesune na anchor slot.
  • FullCalendar planner zobrazuje úklid a rezervace jako samostatné události nad 30min mřížkou; read model proto dál posílá cleanupBlocks v minutách od začátku planner dne.
  • Detail služby obsahuje sekci Homepage, která ukládá isFeaturedOnHomepage a homepageSortOrder; tato pole neovlivňují booking flow ani ceník, jen výběr doporučených služeb na /.
  • src/features/admin/lib/admin-services.ts teď do detailového read modelu přibírá i posledních 10 priceChangeLogs včetně aktéra, aby drawer mohl audit ceny zobrazit bez další klientské fetch vrstvy.
  • Ve formuláři detailu služby (admin-service-form.tsx) je textová vrstva sjednocená pod Veřejná prezentace; publicIntro je jediný zdroj krátkého textu pro web i rezervační flow, aby se stejný copy neudržoval ve dvou polích.
  • Aktuální seznam služeb je záměrně group-first a compact-first:
    • admin-services-list.tsx skládá služby do kategorií
    • service-category-group.tsx drží rozbalovací skupinu jedné kategorie
    • service-compact-row.tsx renderuje hustý pracovní řádek služby
    • service-actions-menu.tsx centralizuje row actions do menu ⋯
    • service-status-badges.tsx drží zjednodušené badge Aktivní/Neaktivní, Veřejná/Interní, volitelně Skrytá
  • KPI strip sekce Služby se musí počítat jen z aktuálně filtrovaného běžného katalogu, ne z celé databáze. Aktuální provozní definice je Veřejné služby = efektivně viditelné položky, Kategorie = počet kategorií v aktuálním pohledu, Interní / skryté = položky neaktivní nebo neveřejné v aktuálním pohledu, Vyžaduje kontrolu = položky s reálným warningem.
  • Toolbar seznamu služeb má zůstat co nejkratší: bez duplicitního mezititulku, bez druhého CTA a se scope běžného katalogu komunikovaným jen přes malé pills. Legenda stavů patří do drobného rozbalovacího prvku Legenda stavů, ne do samostatného vysokého bloku nad filtry.
  • Při dalších úpravách sekce Služby drž prioritu na hustotě seznamu: základní řádek nemá nést dlouhé vysvětlující texty ani plný seznam akcí.
  • Sekundární provozní kontext služby patří do rozbalené části řádku nebo do pravého detail draweru, ne do výchozího stavu seznamu.
  • Sekce Kategorie služeb má vlastní workflow v src/features/admin/components/admin-service-categories-page.tsx a stejně jako Služby obchází generický placeholder renderer.
  • src/features/admin/lib/admin-service-categories.ts drží serverový read model pro seznam, warningy, detail kategorie a počty navázaných služeb podle stavu.
  • Nové UI komponenty pro workflow kategorií jsou v src/components/admin/categories/:
    • CategoryManagementWorkspace.tsx
    • CategoryStats.tsx
    • CategoryFilters.tsx
    • CategoryList.tsx
    • CategoryRow.tsx
    • CategoryDetailPanel.tsx
    • CategoryDetailDrawer.tsx
    • types.ts
  • Aktuální vizuální verze sekce Kategorie služeb je záměrně blíž finálnímu provoznímu mockupu:
    • CategoryStats.tsx renderuje kompaktní souhrnnou lištu
    • CategoryRow.tsx drží hustší řádkový layout s akcemi vpravo
    • CategoryDetailPanel.tsx je klasický formulářový detail se sticky footrem, ne vysoký card stack
    • CategoryDetailPanel.tsx už neobsahuje samostatné pole Veřejný název; kategorie se napříč adminem a veřejným katalogem opírá jen o name
  • src/features/admin/actions/service-category-actions.ts nově obsluhuje create, update, optimistic quick toggles, inline reorder i bezpečné mazání prázdné kategorie; validace zůstává v src/features/admin/lib/admin-service-category-validation.ts.
  • src/features/admin/components/admin-booking-detail-page.tsx a route dvojice /admin/rezervace/[bookingId] + /admin/provoz/rezervace/[bookingId] drží první produkční workflow pro práci s rezervací.
  • Sekce Klienti má vlastní workflow v src/features/admin/components/admin-clients-page.tsx a už neběží přes generický placeholder renderer.
  • src/features/admin/lib/admin-clients.ts drží serverový read model pro seznam klientek, filtry, detail klientky a napojení na historii rezervací.
  • Přehled klientů je záměrně CRM-kompaktní: horní statistiky jsou nízký 4položkový strip (celkem, nové za 30 dní, bez kontaktu, s poznámkou), quick filtry se ukládají do quick search parametru a kombinují se se stávajícím hledáním, stavem i řazením.
  • Desktopový seznam klientů drž jako tabulkový pracovní seznam, ne jako card dump; dlouhé e-maily a technické názvy musí zůstat v truncate kontejnerech. Chybějící kontakt rozlišuj explicitně na bez e-mailu, bez telefonu a bez kontaktu.
  • Testovací klientské profily se v seznamu pouze označují badge test podle bezpečných signálů v read modelu (example.com, Voucher Klientka, Kolize, booking-voucher, client-collision); nepřidávej mazání ani destruktivní bulk akce.
  • src/features/admin/actions/client-actions.ts je tenký server action adaptér pro editaci interní poznámky klientky; validace zůstává v src/features/admin/lib/admin-client-validation.ts.
  • Sekce Média má vlastní workflow v src/features/admin/components/admin-media-page.tsx a je dostupná v owner i salon oblasti na /admin/media a /admin/provoz/media.
  • Server action adaptéry pro média jsou v src/features/admin/actions/media-actions.ts; validace vstupu je v src/features/admin/lib/admin-media-validation.ts.
  • Sekci Média drž jako kompaktní pracovní plochu: krátký header, menší statistické boxy, upload panel s dropzónou, tabs s počty a hustší grid 2-3 karet podle šířky viewportu.
  • Admin karty médií mají kromě typu a publish stavu i text Použití, aby obsluha rovnou viděla, zda asset patří do O mně, Studia, Kontaktu nebo budoucích hero/banner bloků.
  • Quick publish/unpublish v knihovně má používat samostatnou updateMediaPublicationAction; po mutaci zachovej aktivní filtr, aby se obsluha nevracela zbytečně na Vše.
  • Sekce Nastavení má vlastní workflow v src/features/admin/components/admin-settings-page.tsx a už neběží přes generický placeholder renderer.
  • Formuláře pro Salon, Rezervace a E-maily a notifikace jsou oddělené do samostatných client komponent a server action adaptérů v src/features/admin/actions/settings-actions.ts.
  • Sekce Nastavení nově obsahuje i owner-only kalendářový workflow:
    • server action updateCalendarFeedAction
    • client komponentu AdminCalendarSettingsForm
    • read model getOwnerCalendarFeedAdminState()
    • veřejný feed /api/calendar/owner.ics
  • Sdílený skeleton formulářů pro Nastavení je v src/features/admin/components/admin-settings-form-ui.tsx; drží společné styly polí, zprávy po uložení a patičku se submit buttonem, aby se neopakoval stejný markup ve třech sekcích.
  • Stránka Nastavení má nahoře krátký orientační blok, který v jedné větě vysvětlí, co patří do Salon, Rezervace a E-maily.
  • Tón admin copy je sjednocený napříč hlavními sekcemi tak, aby zůstal klidný, krátký a srozumitelný pro běžnou obsluhu.
  • Pro budoucí upload workflow nepřidávej zvláštní implementaci pro owner a salon admin; sdílená feature vrstva je v src/features/media/lib/media-library.ts.
  • Produkční slot routy jsou explicitní a nepoužívají generický [section] detail:
    • /admin/volne-terminy
    • /admin/volne-terminy/[slotId]
    • /admin/volne-terminy/[slotId]/upravit
    • salon varianta pod /admin/provoz/volne-terminy/*
  • src/features/admin/actions/booking-actions.ts je tenký server action adaptér pro změnu stavu i samostatnou editaci interní poznámky rezervace; aktéra mapuje z admin session na reálné AdminUser.id podle e-mailu a při nenalezení používá null, aby zápis historie nenarazil na FK.
  • src/features/admin/components/admin-booking-status-form.tsx používá pro volbu změny stavu klikací akční karty (ne select); vybraná akce se okamžitě zvýrazní barvou podle typu změny a do server action se posílá přes hidden targetStatus.
  • src/features/admin/lib/admin-booking.ts drží detailový read model, mapování povolených přechodů, samostatnou poznámkovou mutaci a zápis do BookingStatusHistory včetně jednoduchého mapování zdroje změny pro timeline.
  • Pohled E-maily v /admin/logy záměrně nezobrazuje placeholder tracking pole Otevřeno a Kliknuto jako plné sloupce; tracking badge vychází z reálných webhook eventů uložených na EmailLog (trackingDeliveredAt, trackingOpenedAt, trackingClickedAt, trackingBouncedAt, trackingFailedAt, trackingSuppressedAt) a bez eventů drží fallback Tracking připraven.
  • Resend webhook endpoint je POST /api/webhooks/resend; streamovaně omezuje raw body na 256 KiB, nad limit vrací 413 a podpis ověřuje přes svix-id, svix-timestamp, svix-signature a RESEND_WEBHOOK_SECRET nad přesnými raw bytes. Event se páruje přes EmailLog.providerMessageId === data.email_id.
  • Pokud používáš Resend delivery tracking, preferuj EMAIL_TRANSPORT=resend; provider při odeslání ukládá Resend email_id do providerMessageId, takže webhook párování je jednoznačné.
  • Resend delivery taxonomie: email.bounced, email.failed a email.suppressed jsou delivery failures (Nedoručeno) a vstupují do aktivního počtu delivery incidentů v chráněné health diagnostice; email.complained znamená, že příjemce označil e-mail jako spam. Complaint je samostatný reputační warning (Nahlášeno jako spam) v historii a Pozornosti, ale není delivery failure. Všechny tyto provider eventy posílají owner Pushover jen při prvním zapsání konkrétního timestampu na EmailLog, aby se předešlo spamování při opakovaných webhook retries.
  • EmailLog zachovává historický transportní i provider delivery stav. Ruční resend zakládá explicitní chain (resendOfId, stabilní resendRootId); webhook email.delivered pro resend idempotentně uzavře incident kořenové zprávy jako DELIVERED_RESEND. OWNER může aktivní root incident ručně uzavřít jako MANUAL s časem, autorem a důvodem, bez založení e-mailu či notifikace. Bounce tedy zůstane v historii jako nedoručený, ale po následně doručeném resendu nebo auditovaném ručním uzavření už nepatří do Pozornost ani do počtu aktivních e-mailových problémů. Samotný SENT, další bounce nebo jiný lifecycle e-mail incident neuzavírá.
  • Meta Další pokus se v přehledu emailů renderuje jen pro stavy pending a retry, aby úspěšně odeslané záznamy zůstaly provozně čitelné.
  • Lifecycle klientských booking e-mailů má dva samostatné kroky, nejde o duplicitní odeslání: veřejná PENDING žádost vytvoří EmailLog.type = BOOKING_RECEIVED se šablonou booking-confirmation-v1 (Přijetí rezervace); teprve přechod na CONFIRMED vytvoří EmailLog.type = BOOKING_CONFIRMED se šablonou booking-approved-v1 (Potvrzení rezervace). Read model pro starší záznamy stále rozlišuje šablony podle templateKey.
  • src/features/admin/components/admin-email-log-detail-page.tsx a route /admin/email-logy/[emailLogId] jsou rozdělené do business-first bloků EmailDetailHeader, EmailStatusBadge, EmailQuickActions, EmailSummaryGrid, EmailLinkedEntities, EmailErrorPanel a EmailTechnicalDetails; všechny drží stejný kompaktní density pattern jako hlavní seznam, quick actions běží jako nízká operační lišta a linked entities používají řádkový layout místo vysokých karet.
  • src/features/admin/lib/admin-data.ts pro detail emailu dopočítává jediný finální stav podle pravidel delivery failure -> Nedoručeno, sentAt -> Odesláno, PENDING + pokusy -> Retry, FAILED -> Selhalo, jinak Čeká, aby se v UI nemohlo potkat současně Odesláno i Retry.
  • Technická data detailu zůstávají server-rendered bez nové client state logiky: rozbalení debug bloku i citlivých údajů používá nativní details/summary.
  • Payload a raw metadata se před zobrazením maskují podle klíčů typu token, hash, secret, signature, key, code; plná data zůstávají dostupná jen po rozbalení Zobrazit citlivá data.
  • Po úspěšné akci detail vrací server-rendered flash banner přes query parametr, aby obsluha viděla okamžitou zpětnou vazbu bez client state.
  • src/features/admin/lib/admin-data.ts je čistá serverová read vrstva pro admin dashboardy a sekce.
  • Admin sekce Rezervace už neřeší jen list/detail/stavové akce; obsahuje i plnohodnotný drawer CreateManualBookingDrawer s provozním formulářem pro ruční vytvoření rezervace.
  • Drawer je rozdělený do menších komponent (BookingClientSelector, BookingServiceSelector, BookingTimeSelector, BookingSourceField, BookingNotificationOptions, BookingInternalNoteField), aby šel stejný workflow později otevřít i z detailu klientky nebo z kalendáře.
  • src/features/admin/actions/booking-actions.ts nově obsahuje i server action createManualBookingAction; pořád je to jen adaptér, skutečný create engine zůstává ve feature vrstvě booking domény.
  • Operativní overview dashboard má vlastní read model mimo admin-data.ts, aby se layout dneška a sekundární sekce nevyvíjely ve stejné obecné struktuře.
  • Dashboard read model src/features/admin/lib/admin-dashboard.ts vrací jen data, která overview skutečně renderuje: akční alerty s prioritou (primary / secondary / ok), kompaktní todayPlanItems, provozní KPI, upcomingSlots, týdenní souhrn a rychlé akce. Když přidáš nové pole, ověř, že má skutečného konzumenta v UI.
  • src/features/admin/components/admin-dashboard-page.tsx skládá overview v pořadí Provozní přehled -> alerty -> KPI -> Dnešní plán / Nejbližší volné termíny -> pravý podpůrný sloupec a drží i serverový skeleton fallback DashboardPageSkeleton.
  • Dnešní dashboardový plán i rozšířená timeline zobrazují u dnešních rezervací existující poznámky s původem Klientka / Interně; bez poznámek nevypisuj placeholder, aby cockpit zůstal rychlý.
  • src/features/admin/components/admin-overview-page.tsx používá Suspense nad async serverovým read modelem, takže overview umí zobrazit loading skeleton bez nové klientské state vrstvy.
  • Lite admin záměrně nepoužívá technický jazyk ani sekce typu nastavení, email logy nebo správa uživatelů.
  • Pro SALON držíme kratší menu a na úvodní obrazovce zviditelňujeme dnešní rezervace, nejbližší termíny a rychlé akce pro přidání slotu nebo otevření rezervace.
  • salonAdminNavigation se skládá ze stejné centrální definice sdílených sekcí jako owner navigace, aby route guardy, dostupné URL a menu nemohly časem ujet od sebe.
  • Dynamické admin routy jako /admin/[section], /admin/provoz/[section] a /admin/email-logy/[emailLogId] mají vlastní layouty se stejným AdminShell, aby se neztratil admin vizuál ani ochrana při přímém vstupu na detailní URL.
  • Sdílené route wrappery pro owner/salon jsou centralizované v src/features/admin/lib/admin-route-factories.tsx; route soubory v src/app/(admin)/admin/**/page.tsx mají být jen tenké entrypointy s předáním area.
  • Sdílený layout wrapper src/features/admin/components/admin-shell-layout.tsx je jediný zdroj truth pro admin shell layout v section/slots/email-log/detail větvích.
  • Vizuální stabilita adminu je primárně v:
    • src/components/layout/admin-shell.tsx (šířky sloupců, sticky sidebar, anti-overflow)
    • src/features/admin/components/admin-page-shell.tsx (responzivní nadpisy, stat karty, spacing pro sekce mimo overview)
    • src/features/admin/components/admin-dashboard-page.tsx (přesná hierarchie a hustota overview dashboardu)
  • Mobilní drawer navigace v admin-shell.tsx má zůstat široká na pohodlné tap targety; při dalších úpravách ji nestlačuj pod aktuální min(92vw, 360px) bez konkrétního důvodu.
  • Při otevřeném mobile draweru v admin-shell.tsx schovávej horní sticky bar; poloprůhledný overlay jinak nechává prosvítat Menu header a vizuálně se pere s navigací.

Konvence

  • Route soubory držet tenké, byznys logiku přesouvat do features, content a lib.
  • Komponenty pojmenovávat podle odpovědnosti, ne podle umístění na stránce typu Section1.
  • Server-side validaci preferovat před klientskými závislostmi.
  • Nezakládat univerzální utils složky ve feature vrstvách bez jasné potřeby.
  • U veřejného webu nepřidávat efektní animace bez jasného UX důvodu.
  • Většinu právních stránek drž na sdílené LegalPage skladbě; finální texty a pořadí sekcí patří do src/content/public-site.ts, ne do rout nebo nahodilých JSX bloků.
  • Výjimkou je /storno-podminky, které používá specializovanou CancellationPolicyPage, protože potřebuje akční hero, praktický kontaktní box a rychlý přehled pravidel ještě před detailními sekcemi.
  • Pokud upravuješ copy storna, drž ho věcně a konkrétně, ale nestrkej do něj tvrdší sankce, které salon reálně neuplatňuje; kanály pro akci mají vždy zmiňovat telefon, e-mail a podle kontextu i odkaz v potvrzení rezervace / reminderu.
  • Pokud právní stránka potřebuje číslovanou osnovu, používej u LegalSection pole eyebrow místo ručního číslování přímo v layout komponentě.
  • Booking mutations držet ve feature service vrstvě a server action používat jen jako tenký vstupní adaptér.
  • Admin změny stavu rezervace validovat server-side proti povoleným přechodům a nikdy je neřídit jen podle toho, co UI zrovna nabízí v selectu.
  • U admin katalogu služeb a kategorií preferuj query-driven vstup do workflow (mode, mobileDetail) před zaváděním nové routy, pokud cílem není nový samostatný workflow.
  • Rychlé provozní akce v seznamech řeš server actions; u kategorií je povolený lehký lokální optimistic state přes useOptimistic, ale bez další state-management knihovny.

Technický dluh a rozhodnutí

  • Klíčová rozhodnutí zapisuj jako krátké ADR záznamy.
  • Uveď důvod, alternativy a dopad.

Datová Vrstva

  • Prisma schema definuje v1 základ pro správu služeb, slotů, klientů a rezervací.
  • Prisma 7 CLI konfigurace je v prisma.config.ts, ne v schema.prisma.
  • Runtime Prisma klient používá @prisma/adapter-pg + pg, protože Prisma 7 vyžaduje pro PostgreSQL explicitní driver adapter.
  • AdminUser zůstává oddělený od klientských kontaktů; klientská vrstva je modelovaná přes Client.
  • AvailabilitySlot je navržený jako ručně publikovatelný termín se stavem zveřejnění; PP Studio má jeden obslužný zdroj, proto capacity musí být vždy 1 a není to model pro více souběžných klientek.
  • Pro admin planner je AvailabilitySlot stále hlavní provozní entita; 30min grid je jen editační vrstva nad souvislými intervaly.
  • AvailabilitySlot má explicitní serviceRestrictionMode, takže admin rozhraní pozná rozdíl mezi slotem bez omezení a slotem, který čeká na výběr služeb.
  • Vazba AvailabilitySlotService umožňuje omezit slot jen na vybrané služby bez zabetonování schématu na jednu službu na slot.
  • Veřejný booking flow rezervuje konkrétní interval uvnitř AvailabilitySlot podle vybraného startsAt a délky služby; planner proto při ukládání půlhodiny stále skládá do souvislých oken.
  • Ruční admin booking používá stejné create jádro jako veřejný booking; rozdíl je jen ve vstupu, volitelných notifikacích a metadatach rezervace.
  • Model Booking nyní nese:
    • source jako kanál vytvoření rezervace (WEB, PHONE, INSTAGRAM, IN_PERSON, OTHER), kde INSTAGRAM znamená ruční rezervaci z Instagram zprávy, ne webovou návštěvu s UTM
    • isManual pro rozlišení admin vytvoření
    • manualOverride pro audit interní výjimky mimo veřejnou dostupnost
  • Planner přímo upravuje jen jednoduché publikované sloty bez rezervací, bez poznámek, bez omezení služeb a s kapacitou 1; ostatní zůstávají v UI viditelné jako uzamčené nebo neaktivní.
  • Pokud slot obsahuje rezervaci jen v části intervalu (např. booking 09:00-09:30 v okně 08:00-14:00), planner ho ve vizualizaci dělí na rezervovanou část a chráněný zbytek, aby celý interval nezmizel z mřížky.
  • Import kategorií a služeb je řešený jako JSON upsert přes scripts/import-services.mjs; identity záznamů drží slug.
  • Jednorázový backfill strukturované copy služeb v DB je oddělený od importu katalogu a běží přes scripts/backfill-service-copy.ts. Skript používá DATABASE_URL, defaultně dělá dry-run, ostrý zápis vyžaduje --confirm, hledá jen známé slugy z service-copy-overrides.ts a zapisuje pouze seoTitle, idealFor, includes, benefits a goodToKnow.
  • Cleanup testovacích booking dat je záměrně oddělený od resetu celé DB: scripts/clear-booking-data.mjs maže rezervace, sloty a navázané provozní logy, ale nechává katalog služeb, admin účty, singleton settings i média.
  • Booking ukládá snapshot jména služby, ceny a času, takže historické rezervace zůstanou konzistentní i po úpravě katalogu.
  • Service nově odděluje obecnou aktivitu (isActive) od veřejné rezervovatelnosti (isPubliclyBookable); public booking flow vyžaduje obě podmínky a aktivní kategorii.
  • Booking drží metadata posledního přesunu (rescheduledAt, rescheduleCount) a reminder queue stavu (reminder24hQueuedAt, reminder24hSentAt); historický self-relation chain zůstává ve schématu jen jako legacy pole a nové reschedule flow ho nepoužívá.
  • BookingRescheduleLog je samostatný auditní model pro doménovou akci přesunu termínu; ukládá původní a nový interval, aktéra a volitelný důvod změny.
  • BookingStatusHistory drží auditní stopu změn stavu včetně aktéra a strukturovaných metadat.
  • ServicePriceChangeLog drží auditní stopu změn ceníku služeb v adminu; zapisuje jen skutečné změny priceFromCzk, ne každý save formuláře.
  • Voucher databázový základ je v migraci 20260427205720_add_vouchers: Voucher eviduje dárkový voucher jako VALUE nebo SERVICE, stav, kupujícího/obdarovanou, hodnotu nebo snapshot služby a auditní vazbu na admin uživatele.
  • VoucherRedemption je jediný důkaz skutečného uplatnění voucheru; jedna rezervace smí mít nejvýše jeden takový záznam, protože uplatnění voucheru znamená provozní úhradu rezervace. Veřejné zadání voucheru má ukládat jen záměr, ne čerpání.
  • Booking má pro MVP voucher intent přímo na sobě přes intendedVoucherId, intendedVoucherCodeSnapshot a intendedVoucherValidatedAt; samostatný BookingVoucherIntent se zatím nezavádí.
  • Další business logiku voucherů drž pod src/features/vouchers; admin UI, public booking napojení a PDF generování nejsou součástí databázové foundation migrace.
  • BookingActionToken ukládá hash tokenu, expiraci a použití/revokaci pro bezpečné self-service storno, self-service změnu termínu a provozní email akce. Raw token je jen dočasně v token-bearing EmailLog.payload pro PENDING/retryable failure outbox; po SENT nebo terminálním FAILED se známá URL pole redigují.
  • CalendarFeed drží owner subscription feed jako samostatnou entitu mimo SiteSettings; ukládá scope, aktivaci, rotační salt a audit času změny.
  • Kalendářový token se neukládá jako raw secret do DB. URL se odvozuje serverově z CalendarFeed.id, tokenSalt a ADMIN_SESSION_SECRET, takže:
    • admin může odkaz zkopírovat kdykoli
    • po rotaci stačí změnit tokenSalt
    • po vypnutí se validace zastaví na isActive = false
  • ICS generátor v src/features/calendar/lib/calendar-ics.ts drží:
    • escapování textu podle RFC 5545
    • line folding po 75 bajtech
    • VTIMEZONE blok pro Europe/Prague
    • oddělený mapper Booking -> VEVENT
  • Stejný model BookingActionToken obsluhuje i owner/provoz email akce APPROVE a REJECT; do e-mailu se posílá raw token, v DB zůstává hash a auditní čas použití nebo revokace. Po úspěšném odeslání nebo terminálním selhání EmailLog.payload zachová jen [REDACTED] v URL polích.
  • Serverová doménová vrstva pro email akce je v src/features/booking/lib/booking-email-actions.ts; drží validaci intentu, serializable transakci, změnu stavu, audit a založení klientského EmailLog.
  • src/features/calendar/lib/booking-calendar-attachment.ts generuje zákaznickou .ics přílohu z e-mailového payloadu; veřejný booking calendar endpoint ani token typu CALENDAR neudržuj.
  • EmailLog je připravený na notifikační workflow a troubleshooting komunikace s klientem.
  • 24h reminder rezervací je v src/features/booking/lib/booking-reminders.ts; scheduler vybírá CONFIRMED bookingy s e-mailem, reminder24hSentAt = null a dosud nezačatým termínem nejvýše now + 26h. Kanonický helper rozliší původní 25–26h okno od catch-up fáze, deduplikuje existující current reminder a pozdní admin confirmation, owner approve i ruční potvrzený booking jej volají přímo ve své transakci.
  • Reminder scheduler neběží jako zvláštní služba; src/lib/email/worker.ts ho spouští uvnitř existujícího email:worker procesu každých 5 minut a vytváří pouze EmailLog, nikdy neodesílá SMTP přímo.
  • Stejný email:worker každých 15 minut spouští bounded cleanup expirovaných RateLimitReservation záznamů po dávce nejvýše 500 řádků; chyba cleanupu nesmí zastavit doručování e-mailů a opakování je bezpečné.
  • Reminder template booking-reminder-24h-v1 je krátký, bez .ics, a používá dvojici bezpečných tokenů pro Změnit termín a Zrušit rezervaci; copy a layout mají držet lidský tón a rychlou scanovatelnost.
  • Idempotence reminderu stojí na kombinaci Booking.reminder24hQueuedAt, Booking.reminder24hSentAt a transakčního claimu kandidátky; při přesunu termínu reschedule flow queue marker resetuje, aby se reminder mohl navázat na nový čas bez duplikace starého jobu.
  • Legacy Setting zůstává v databázi jako obecné key-value úložiště pro budoucí interní potřeby, ale produkční admin sekce Nastavení stojí na explicitním singleton modelu SiteSettings.
  • src/lib/site-settings.ts je centrální read vrstva pro veřejné kontakty, booking pravidla a e-mailový branding; veřejné a e-mailové read cesty už do DB nezapisují a při chybě nebo chybějícím singletonu spadnou na bezpečné defaulty z env/content vrstvy.
  • Bootstrap SiteSettings singletonu zůstává záměrně jen v owner admin workflow Nastavení přes explicitní ensureSiteSettings(), takže public metadata, e-mail šablony ani testy nespouštějí write path při obyčejném čtení.
  • SiteSettings drží jen skutečně globální provozní hodnoty. Technické env proměnné jako SMTP host/port, NEXT_PUBLIC_APP_URL nebo ADMIN_SESSION_SECRET se do adminu záměrně nepřenášejí.
  • SiteSettings.voucherPdfLogoMediaId je nullable legacy FK na MediaAsset zachovaná kvůli kompatibilitě. Aktuální master PDF, admin Settings ani e-mailový renderer tuto vazbu nepoužívají a PDF templates se do Media Manageru nepřidávají.
  • MediaAsset je obecný metadata model pro certifikáty, fotky prostor, reference i další obsahové obrázky; binární obsah zůstává na lokálním filesystemu mimo DB.
  • Veřejná stránka /studio používá read model src/features/public/lib/public-studio-photos.ts, který smí vracet jen MediaType.SALON_PHOTO s isPublished = true; komponenty stránky jsou v src/features/public/components/studio/studio-page.tsx.
  • Route src/app/(public)/studio/page.tsx je aktivní a renderuje StudioPage s daty z getPublicStudioPhotos(); stránka už není schovaná přes notFound().
  • Prolinkování /studio do veřejného webu je řízené přes mainNavigation v src/config/navigation.ts; položka se automaticky propíše do SiteHeader i do footer sekce Navigace.
  • Filesystem layout médií má pro nové uploady tvar <MEDIA_STORAGE_ROOT>/public/<type>/<year>/<month>/<assetId>-<variant>.<ext>; používáme fixní YYYY/MM, bez jednorázových složek a bez názvů od uživatele v storedFilename.
  • src/lib/media/local-media-storage.ts je adapter pro lokální filesystem; business vrstva přes něj neřeší konkrétní fs operace ani fyzické cesty.
  • src/lib/media/media-pipeline.ts drží lehkou server-side pipeline nad sharp; pro JPEG/PNG/WebP dělá EXIF auto-rotate už na ukládaném originálu, optimized variantu (max 1920 px) a thumbnail variantu (cca 400 px) bez zavádění CDN nebo komplexního responsive systému.
  • Route /media/[kind]/[[...path]] používá v media path helperu a storage adapteru cílené turbopackIgnore anotace u dynamických path.resolve/path.join; cílem je zabránit tomu, aby Next.js 16 Turbopack NFT tracer při buildu omylem zahrnoval celý projekt.
  • Kanonická veřejná URL pro nové uploady je /media/public/<type>/YYYY/MM/<filename>; legacy route /media/[kind]/[[...path]] zůstává kvůli starším médiím.
  • Obě veřejné media routy pouze reexportují GET ze src/lib/media/public-media-route.ts; zde zůstává jediná kontrola bezpečné cesty, publikace assetu a bezpečných response hlaviček.
  • Route pro média umí vrátit originál i varianty optimized / thumbnail podle konkrétní storage path uložené v MediaAsset; starší záznamy bez variant fungují dál přes fallback na původní storagePath.
  • src/lib/media/media-validation.ts centralizuje kontrolu MIME typu, přípony a maximální velikosti souboru.
  • src/lib/media/media-filename.ts generuje krátký náhodný asset key a stabilní suffixy original, optimized, thumbnail, takže naming zůstává konzistentní a připravený na další varianty.
  • src/features/booking/lib/booking-public.ts je veřejný write model pro rezervace a drží i ochranu proti souběžnému obsazení slotu.
  • Veřejný booking write model ukládá k rezervaci i akviziční metadata (acquisitionSource, acquisitionReferrerHost, acquisitionUtmSource, acquisitionUtmMedium, acquisitionUtmCampaign), pokud jsou dostupná. Admin UI je má prezentovat jako Odkud přišla, aby se nemíchala s kanálem Web.
  • Veřejná rezervace se po submitu vytváří jako BookingStatus.PENDING; potvrzení (CONFIRMED) je provozní krok z adminu.
  • Pokud veřejná rezervace zabere jen část delšího slotu s kapacitou 1, booking write model slot v transakci automaticky rozdělí na rezervovaný úsek a zbylé volné fragmenty, aby admin planner zůstal editovatelný po samostatných blocích.
  • src/features/booking/lib/booking-cancellation.ts drží veřejné storno workflow nad hashovaným action tokenem.
  • GET načtení /rezervace/sprava/[token] nesmí vytvářet nový CANCEL token. Storno URL se vydává až při explicitní server action startPublicBookingCancellationAction(...), protože DB záměrně drží jen hash tokenu a existující raw token nelze bezpečně rekonstruovat.
  • Mazání BookingPayment používej přes deleteBookingPaymentWithAudit(...); kromě smazání platby zapisuje provozní audit do BookingStatusHistory s payment metadaty a admin aktérem.
  • Admin route handlery s mutací musí používat isSameOriginAdminRequest(...) nebo ekvivalentní Origin/Host kontrolu proti NEXT_PUBLIC_APP_URL; Server Actions zůstávají na vestavěné Next origin ochraně a existujícím session/role ověření.
  • E-mailové subject/from-name hodnoty validuj přes src/lib/email/header.ts. CRLF je zakázané, diakritika zůstává povolená a délkové limity řeší konkrétní formulář/schema.
  • src/features/public/lib/public-certificates.ts je veřejný read model certifikátů pro stránku /o-mne; smí vracet jen MediaType.CERTIFICATE a isPublished = true.
  • src/features/public/lib/public-media.ts drží sdílené read helpery pro publikované obrázky podle typu; homepage čte MediaType.PORTRAIT_HOME a /o-mne čte MediaType.PORTRAIT_ABOUT; legacy MediaType.PORTRAIT už veřejný web nepoužívá.
  • src/features/public/lib/public-studio-photos.ts je veřejný read model fotek studia; používá ho /studio pro hero + galerii a /kontakt pro hero fotografii.
  • /studio používá první dostupnou fotku jako hero a následující dostupné fotky jako galerii (max 6), aby se úvodní vizuál neopakoval.
  • Galerie /studio má responzivní grid pro 1-6 navazujících fotek; při prázdné galerii se sekce obrázků skryje a při chybějícím DB alt textu se používá Fotografie prostoru PP Studio.
  • /kontakt používá pouze MediaType.CONTACT_PHOTO; pokud kontaktní fotka chybí, hero zůstane u placeholderu a nesahá do SALON_PHOTO.
  • src/features/public/lib/public-studio-photos.ts při čtení SALON_PHOTO filtruje publikované záznamy i podle fyzické existence souboru ve storage (optimizedStoragePath/storagePath), takže veřejný web nezobrazuje broken image pro orphan DB záznamy.
  • Dev-only fallback pro /studio je povolený jen při NODE_ENV=development přes public/dev/studio/*; produkce vždy čte jen reálná média z DB/storage.
  • Admin media upload na aktivním filtru používá stejný MediaType jako výchozí hodnotu selectu; pro studio fotky tedy nejdřív otevři filtr Prostory.
  • Pro kontaktní hero fotku otevři v admin media filtr Kontakt; nový upload se tím založí jako MediaType.CONTACT_PHOTO.
  • MediaAsset.sortOrder je upravitelný v admin media formulářích a veřejný read model jej respektuje přes existující řazení sortOrder ASC, createdAt DESC.
  • Server actions pro média po uploadu, editaci, publish/unpublish i smazání revalidují /studio a /kontakt, protože obě stránky čtou veřejnou media knihovnu (SALON_PHOTO pro studio, CONTACT_PHOTO pro kontakt).
  • Veřejný booking flow vrací doménové chybové kódy a doporučený krok formuláře, takže UI může zobrazit přesnější recovery stav bez duplikace serverové logiky.
  • Veřejný booking submit má lehký rate limit podle IP a e-mailu a zapisuje auditní log pokusů, blokací a selhání pro provozní troubleshooting.
  • Krok 2 veřejného booking flow filtruje sloty i podle délky služby, aby se krátké sloty neukazovaly až v posledním kroku.
  • Krok 1 veřejného booking flow je dvouúrovňový (kategorie -> služba), ale dál používá stejný katalog a serviceId.
  • Krok 2 veřejného booking flow nabízí nejdřív SuggestedSlots s nejbližšími volnými časy a teprve pod nimi kalendářní fallback pro jiný den.
  • Krok 2 veřejného booking flow používá dvoufázový výběr termínu: rychlá doporučená volba nebo kalendářní výběr dne a následně seznam konkrétních časů pro vybraný den.
  • Krok 2 generuje konkrétní starty po 30 minutách uvnitř slotu a zobrazuje jen ty, které se při aktuální kapacitě nekryjí s existujícími aktivními rezervacemi.
  • Krok 2 veřejného booking flow drž jako rychlý decision flow: doporučené termíny nahoře, kalendář jako fallback a pod ním větší tlačítka konkrétních časů; detail termínu patří až do souhrnu v pravém panelu.
  • Kontaktní krok používá lehkou klientskou inline validaci jen jako UX vrstvu; server-side validace v create-public-booking.ts zůstává autoritativní.
  • Veřejná route /rezervace může dostat ?service=<slug>; klient smí předvybrat jen službu, kterou najde v právě načteném veřejném katalogu. Nesmí vzniknout samostatná trust větev přes service ID nebo jiný bypass katalogu.
  • create-public-booking.ts kromě IP/user-agent auditu načítá i cookie ppstudio-booking-acq a propsává akviziční kontext do Booking i BookingSubmissionLog.metadata.
  • Klientský tracker src/features/booking/components/booking-acquisition-tracker.tsx běží v root layoutu, sbírá utm_* + externí document.referrer, normalizuje je a ukládá do cookie ppstudio-booking-acq (SameSite=Lax, 30 dní).
  • Akviziční cookie ukládá pouze relativní landingPath; scheme-relative hodnoty typu //host/path a backslash varianty se zahazují na /. Referrer hosty se klasifikují jen přes přesnou doménu nebo subdoménu, ne přes volný substring.
  • Pokud landing URL kombinuje service s utm_* nebo mtm_*, route ani klientský flow nesmí query přepsat; tracker má zachovat původní landingPath včetně marketingových parametrů.
  • Success stav veřejného booking flow drž jako vlastní confirmation layout, ne jako prodloužený souhrn:
    • horní status blok jen pro stav rezervace, s copy Rezervace přijata a Čeká na finální potvrzení
    • hero text má výslovně říct, že termín je pro klientku předběžně rezervovaný
    • hlavní detail rezervace ukazuje samostatně službu, datum a čas; čas zobrazuj s mezerami kolem pomlčky, například 09:30 – 10:30
    • density confirmation vrstvy drž kompaktní: menší vertikální mezery, nižší hero i detail card, bez dlouhých samostatných bloků
    • detail rezervace preferuj ve skladbě služba + datum · čas; čas zůstává vizuálně nejvýraznější údaj
    • referenční kód nezobrazuj, dokud neexistuje samostatné business pole používané v adminu nebo klientské komunikaci
    • stručný blok Co bude následovat má říct, že potvrzení přijde e-mailem a studio se ozve při potřebě upřesnění
    • pod další kroky patří krátké uklidnění, že termín je rezervovaný a klientka nemusí dělat nic dalšího
    • blok Potřebujete změnu?, CTA Změnit termín a CTA Zrušit rezervaci na post-submit screen nevracej; tahle obrazovka má flow uzavírat, ne otevírat další rozhodnutí
    • intro aktivního flow Vyberte si termín... renderuj jen před formulářem, ne nad confirmation panelem po úspěšném submitu
    • kontakt na studio až v posledním bloku; na desktopu může být v jedné řádce email · telefon, na mobilu jako dvě dobře klikatelné akce
    • booking varianta shellu může mít kompaktnější footer než ostatní veřejné stránky, ale bez změny linků a kontaktů
  • createPublicBooking() vrací pro confirmation vrstvu i scheduledStartsAt, scheduledEndsAt a cancellationUrl, aby web i e-mail nemusely domýšlet další akce z neúplných dat.
  • BookingConfirmationPanel tokenové manage/cancel odkazy z public action payloadu nemění ani negeneruje, ale na post-submit obrazovce je nezobrazuje; bezpečný manage entrypoint zůstává pro e-maily a detail rezervace.
  • Matomo event Rezervace / Kontakt zahájen se nově posílá až při první reálné interakci s kontaktním polem (focus/input), ne při samotném výběru času; eventy Kontakt pole fokus, Kontakt pole vyplnění začátek a Kontakt pole chyba jsou per-field omezené na první výskyt.
  • Matomo event Rezervace / Vytvořena se posílá po success stavu v BookingFlow a chrání ho createdBookingTrackedRef; nepřidávej další odeslání přímo do BookingConfirmationPanel, aby nevznikaly duplicity při re-renderu.
  • Matomo event Rezervace / Služba vybrána musí odcházet i při předvyplnění služby z URL (/rezervace?service=...) bez ručního kliku ve kroku služby; v BookingFlow je na to samostatný jednorázový guard prefilledServiceTrackedRef, aby funnel zachytil vstupy z ceníku/detailu služby bez duplicit.
  • Self-service změna termínu v BookingManagementPanel používá stejný princip: Rezervace / Datum vybráno a Rezervace / Čas vybrán se volají jen v click handlerech, refy brání opakovanému odeslání stejné volby a event name nesmí obsahovat token, klientku ani kontakt.
  • /rezervace/sprava/[token] drž jako produkční decision flow: hero kontext, kompaktní aktuální rezervace, primární nejbližší termíny, sekundární kalendář, sloty pro vybraný den, potvrzení a až nakonec slabé storno. Telefon ani další CTA nevracej vedle potvrzení.
  • Po kliknutí na den má UI plynule scrollovat na sloty vybraného dne; po kliknutí na slot má nastavit hidden inputs, zvýraznit čas, aktualizovat potvrzení a scrollovat na sekci Potvrdit nový termín bez klientského redirectu.
  • Pending confirmation screen po odeslání rezervace záměrně nenabízí Přidat do kalendáře; kalendářová událost se přikládá až do emailu booking-approved-v1 po přechodu rezervace do CONFIRMED.
  • Transformaci slotů pro krok 2 drž mimo JSX v helperu src/features/booking/lib/booking-time-slots.ts; UI komponenty mají dostávat už připravené TimeSlotOption[] a skupiny z groupSlotsByDayPeriod().
  • Kalendářní denní klíče v kroku 2 (YYYY-MM-DD) generuj locale-agnosticky přes Intl.DateTimeFormat(...).formatToParts(); nepoužívej format() jako zdroj klíče, protože pořadí/oddělovače se liší mezi prostředími a může rozbít mapování měsíců/dnů.
  • U kalendářních gridů v kroku 2 drž explicitní gridTemplateColumns: repeat(7, minmax(0, 1fr)) přímo v komponentě jako runtime pojistku; samotná utility třída nemusí v některých prostředích stačit.
  • src/lib/email/* je samostatná infrastrukturní vrstva:
    • provider řeší SMTP transport
    • templates renderují obsah z EmailLog.templateKey
    • worker claimuje EmailLog řádky v background režimu a delivery aktualizuje EmailLog.status, provider, providerMessageId, attemptCount, nextAttemptAt a errorMessage
  • Renderer klientských šablon musí být kompatibilní i se staršími EmailLog.payload: u booking-confirmation-v1, booking-approved-v1, booking-reminder-24h-v1 a booking-rescheduled-v1 je manageReservationUrl volitelný fallback; při chybějící hodnotě se nesmí rozbít render ani worker, jen se vynechá případné CTA Změnit termín.
  • Self-service přesun rezervace (changedByClient=true) zakládá vedle klientského booking-rescheduled-v1 i admin notifikaci admin-booking-rescheduled-v1 na notificationAdminEmail (pokud je nastavený), aby owner dostal informaci o přesunu mimo admin UI.
  • Admin šablona admin-booking-notification-v1 má zůstat email-safe a mobilně rozhodovací: inline styly, tabulková plnošířková CTA, Arial/Helvetica pro tlačítka, bez web fontů, bez přehnaného letter-spacing a bez dlouhého vysvětlování procesu. Může obsahovat clientNote jako provozní kontext pro schválení čekající rezervace, ale klientské booking e-maily zákaznickou poznámku dál neposílají. Neměň approve/reject tokenové URL ani adminUrl; Přesunout termín vede na existující detail rezervace v administraci.
  • Potvrzovací e-mail booking-confirmation-v1 má stejně jako webový post-submit screen držet hierarchii bez CTA: stav -> služba / datum / čas -> místo -> kontakt.
  • booking-reminder-24h-v1 nemá samostatné CTA Ozvat se studiu; kontakt je jednou ve spodním kontaktním bloku a akce Změnit termín / Zrušit rezervaci zůstávají sekundární.
  • U klientských e-mailů nesmí být storno vizuálně dominantnější než obsah potvrzení nebo připomínky. U admin notifikace je jediná primary akce Potvrdit rezervaci; Přesunout termín a Otevřít v administraci jsou secondary, Zrušit rezervaci danger-light.
  • booking-confirmation-v1, booking-approved-v1, booking-reminder-24h-v1 i booking-rescheduled-v1 teď dostávají manageReservationUrl; token se generuje per e-mail/send, PENDING/retryable FAILED outbox drží raw URL pro delivery/retry a po SENT nebo terminálním FAILED se URL v payloadu redigují. Ruční resend vydává nové tokeny místo kopírování historického payloadu.
  • Referenční kód rezervace už se v klientském flow záměrně nepoužívá; veřejný web, e-maily i .ics popis komunikují jen službu, termín a konkrétní akce přes tokenizované odkazy.
  • Potvrzovací e-mail booking-approved-v1 nově přikládá soubor pp-studio-rezervace.ics; attachment se generuje serverově při renderu šablony z payloadu bookingId + serviceName + scheduledStartsAt + scheduledEndsAt.

Migrační Strategie

  • Stávající bootstrap migrace rozšiřujeme inkrementálně, ne přepisem historie.
  • Migrace 20260418184500_schema_v1_booking_core zachovává existující booking data:
    • vytvoří Client z historických rezervací
    • převádí BookingRequest na Booking
    • backfilluje snapshot služby a času
    • převádí single-service sloty na M:N omezení služeb
  • Migrace 20260418193000_booking_model_review_fixes doplňuje minimální provozní ochrany:
    • explicitní režim omezení služeb na slotu
    • historický reschedule chain pro rezervace (dnes už jen legacy pole)
    • původní unique ochranu proti duplicitní rezervaci stejného klienta do stejného slotu (později nahrazenou přesnější variantou na úroveň konkrétního intervalu)
    • PostgreSQL exclusion constraint proti překrývajícím se aktivním slotům
  • Migrace 20260420153000_booking_exact_duplicate_active nahrazuje široké UNIQUE(slotId, clientId) za partial unique index Booking_exact_duplicate_active_key, který blokuje jen přesně duplicitní aktivní interval (slotId + clientId + scheduledStartsAt + scheduledEndsAt při status IN (PENDING, CONFIRMED)).
  • Migrace 20260710110000_availability_slot_capacity_one nejdřív spočítá sloty s capacity <> 1 a při nenulovém výsledku se ukončí bez změny dat. Teprve nad čistými daty nahrazuje původní CHECK capacity > 0 constraintem capacity = 1.
  • Migrace 20260423113000_booking_reschedule_logs_v1 přidává Booking.reminder24hQueuedAt, Booking.rescheduleCount a nový auditní model BookingRescheduleLog pro doménovou akci přesunu termínu.
  • Migrace 20260424103000_service_price_change_log_v1 přidává auditní model ServicePriceChangeLog pro změny cen služeb v adminu.
  • Nové slot admin workflow nevyžadovalo další migraci; navazuje přímo na už existující schema a constrainty.
  • Migrace 20260419103000_service_public_bookability přidává Service.isPubliclyBookable a backfilluje ho podle dosavadního isActive, aby se zachovalo chování migrovaných služeb.
  • Při další iteraci booking workflow preferuj nové migrace nad ruční editací starších SQL souborů.

Testování

  • Minimální kontrola při každé změně:

    • npm run lint
    • npm run typecheck
    • npm run test
    • npm run build
  • npm run test nyní skládá test:unit a následný test:db:integration; guard RUN_DB_INTEGRATION_TESTS=1 proto aktivuje DB scénáře jen ve výslovně určené, sériové vrstvě.

  • U DB integračních testů, které vytváří AvailabilitySlot řádky, neseeduj časy z malého fixního okna; při paralelním běhu to může náhodně narážet na AvailabilitySlot_active_time_window_excl. Preferuj UUID/hash odvozený časový rozptyl uvnitř aktuálního booking window.

  • Stejné pravidlo platí i pro seed aktivních Booking, ne jen slotů. Pokud test vybírá konkrétní startsAt ručně, musí předem ověřit i absenci překryvu s jinou aktivní rezervací; jinak se flake projeví jako doménová chyba Vybraný termín koliduje s jinou rezervací. místo skutečné regresní změny.

  • Pro DB-backed integrační testy booking domény je připravený i:

    • npm run test:db:booking
  • Pro celou DB integrační vrstvu (včetně admin a voucher scénářů) použij:

    • npm run test:db:integration
  • npm run test:db:booking spouští celý booking integrační glob src/features/booking/lib/*.integration.test.ts, takže vedle reschedule/management scénářů pokrývá i veřejné vytvoření rezervace, ruční admin booking a 24h reminder + e-mail worker flow.

  • Pro rychlé unit ověření bezpečné veřejné správy rezervace a reschedule domény můžeš spustit i:

    • node --import tsx --test src/features/booking/lib/booking-management.test.ts
    • node --import tsx --test src/features/booking/lib/booking-rescheduling.test.ts
  • src/features/booking/lib/booking-management.ts a src/features/booking/lib/booking-rescheduling.ts mají záměrně malé dependency factories createBookingManagementApi(...) a createBookingReschedulingApi(...); slouží jen jako test seam pro mocknutí Prisma/notifikačních závislostí a nemají být druhým produkčním entrypointem s odlišnou business logikou.

  • Veřejný klientský reschedule nesmí obcházet pravidla přes admin-style manual override; integrační test pro /rezervace/sprava/[token] musí ověřovat i odmítnutí termínu mimo online okno, stejného termínu, kolize a terminal stavů bez zápisu historie nebo email side effects.

  • npm run dev i npm run build nyní před startem automaticky spouští prisma generate, takže po změně Prisma schématu nevznikne rozjezd mezi generovaným klientem a runtime admin obrazovkami.

  • Pokud měníš e-mail delivery, ověř i npm run email:worker:once.

  • Po změně reminder flow ručně ověř i:

    • že npm run email:worker:once zapíše reminder kandidátky do EmailLog
    • že reminder e-mail nevytváří .ics attachment
    • že storno nebo přesun rezervace mezi enqueue a sendem skončí system-skip, ne reálným odesláním
    • že ruční owner akce Znovu odeslat e-mail na reminder detailu vytvoří nový log s payload flagem manualReminderResend=true a tenhle resend se neposuzuje běžným reminder preflight skip pravidlem
  • Před aplikací migrací v prostředí, kde už běžela produkční data, spusť npm run db:check-migrations; script zkontroluje otevřené failed/incomplete záznamy v _prisma_migrations.

  • Před migrací kapacity navíc proveď SELECT "id", "startsAt", "endsAt", "status", "capacity" FROM "AvailabilitySlot" WHERE "capacity" <> 1;. Nalezené řádky rozhodni a oprav explicitně; migrace je nikdy sama nesnižuje, aby nezakryla možné souběžné rezervace.

  • Při změně veřejného webu navíc ručně ověř:

  • Po změně veřejného booking flow ručně ověř i:

    • /rezervace na mobilu i desktopu
    • přepnutí kategorie služby a reset vybraného termínu při změně služby
    • automatický scroll ze služby na termíny
    • sekci Nejbližší dostupné termíny a jedním klikem navázaný přechod na kontakt
    • kalendářní fallback pro jiný den
    • větší grid časů včetně selected/disabled stavů
    • inline validaci jména, e-mailu a telefonu
    • sticky CTA na mobilu pro stavy vybrat termín -> doplnit kontakt -> odeslat rezervaci
    • editaci služby / termínu / kontaktu ze souhrnu bez ztráty vybraných dat
  • Po změně admin katalogu služeb ručně ověř i:

    • /admin/sluzby i /admin/provoz/sluzby na desktopu a mobilu
    • založení nové služby přes CTA Nová služba
    • filtr podle kategorie a kombinaci s fulltextem / stavem / veřejností
    • rychlé akce v kartě seznamu (aktivovat, veřejná/interní, duplikovat, posunout)
    • mobilní otevření detailu a návrat zpět na seznam
  • Po změně admin katalogu kategorií ručně ověř i:

    • /admin/kategorie-sluzeb i /admin/provoz/kategorie-sluzeb na desktopu a mobilu
    • pravý detail drawer na desktopu i mobilu
    • vytvoření nové kategorie přes CTA + Nová kategorie
    • kombinaci fulltextu, stavu, řazení a chip filtrů
    • optimistic přepnutí aktivního stavu a posun nahoru/dolů
    • warning stavy prázdná, bez veřejné služby, neaktivní s aktivními službami
  • Po změně sekce Rezervace ručně ověř i:

    • /admin/rezervace i /admin/provoz/rezervace na desktopu a mobilu
    • kompaktní řádkový layout bez návratu k vysokým kartám
    • sticky header sloupců při scrollu
    • inline akce Potvrdit a Zrušit bez otevření detailu a zkrácené CTA Otevřít
    • sloupec Status jako samostatný centrovaný grid item
    • CANCELLED se ve sloupci Status jen lehce podbarvuje červeně pro rychlejší skenování
    • ověření, že akce na menších šířkách fungují jako full-width footer a od lg se vrací do úsporného vlastního sloupce
    • správné schování neplatné rychlé akce podle aktuálního stavu rezervace
    • barevné badge pro čeká, hotovo, zrušeno a čitelnost kontaktu i zdroje v hustém řádku
    • detail rezervace s novou kompaktní statickou hlavičkou, jedním souhrnným panelem a kompaktní akční zónou
    • samostatné uložení interní poznámky bez změny stavu a propsání do historie
    • timeline historie s důvodem, poznámkou a zdrojem změny, pokud je k dispozici
  • Admin sekce Služby i Kategorie služeb nyní používají sjednocený pravý overlay drawer pattern i na desktopu:

    • Služby: query-driven výběr služby nebo mode=create otevře pravý drawer nad seznamem
    • Kategorie: list/detail workspace už nepoužívá sticky desktop panel; detail se otevírá přes CategoryDetailDrawer pro všechny breakpointy
    • přepnutí Veřejně rezervovatelná a dopad na /rezervace
    • změnu délky služby a skrytí slotů, které jsou po změně kratší než služba
    • přepínání editace mezi službami z různých kategorií a správné předvyplnění konkrétní vybrané služby
    • editaci služby v neaktivní kategorii a očekávané skrytí z veřejného bookingu
  • Po změně admin kategorií služeb ručně ověř i:

    • /admin/kategorie-sluzeb i /admin/provoz/kategorie-sluzeb na desktopu a mobilu
    • založení nové kategorie přes CTA Nová kategorie
    • rychlé akce v seznamu (aktivovat, posunout, zobrazit služby)
    • CTA Vytvořit službu v této kategorii a předvyplnění kategorie v sekci Služby
    • mobilní otevření detailu a návrat zpět na seznam
    • změnu pořadí kategorie a nové řazení na /sluzby, /cenik a v /rezervace
    • deaktivaci kategorie s navázanými službami a očekávané skrytí z veřejného webu i bookingu
    • blokaci mazání neprázdné kategorie a úspěšné smazání prázdné kategorie
  • Po změně admin sekce Nastavení ručně ověř i:

    • /admin/nastaveni na desktopu i mobilu
    • propsání kontaktů do footeru a stránky /kontakt
    • propsání storno limitu do /faq a /storno-podminky
    • propsání booking pravidel do /rezervace a do self-service storna
    • novou rezervaci i storno s admin notifikačním e-mailem
    • mobilní header a CTA na /, /sluzby, /kontakt
    • správnost interních odkazů v footeru
    • metadata a titulky pro detail služby
  • Po změně Prisma schematu navíc:

    • npm run db:generate
    • npm run db:migrate
    • npx prisma validate --schema prisma/schema.prisma
  • Po změně admin voucher detailu ručně ověř owner i salon variantu /admin/vouchery/[voucherId] a /admin/provoz/vouchery/[voucherId]: provozní editace smí ukládat jen kupujícího, e-mail, platnost a interní poznámku; zrušení musí vyžadovat důvod, odmítnout vouchery s čerpáním a po zrušení schovat opakovanou akci.

  • Po změně voucher ověření nebo čerpání ručně ověř, že CANCELLED má prioritu před expirací a zůstatkem, veřejná stránka /vouchery/overeni neukáže interní důvod zrušení a admin detail důvod zobrazí.

  • Po změně slot admin workflow ručně ověř i:

  • Po změně detailu rezervace ručně ověř i:

    • /admin/rezervace/[bookingId] a /admin/provoz/rezervace/[bookingId] na desktopu i mobilu
    • stavy PENDING, CONFIRMED i uzavřenou rezervaci bez dostupných dalších akcí
    • funkčnost rychlých odkazů tel: a mailto:
    • propsání uložené změny do success stavu formuláře i do bloku Historie změn
  • Po změně admin sekce Klienti ručně ověř i:

    • /admin/klienti a /admin/provoz/klienti na desktopu i mobilu
    • hledání podle jména, e-mailu i telefonu
    • přechod do detailu klientky a návrat zpět na seznam
    • uložení interní poznámky a propsání změny po refreshi do owner i salon oblasti
    • rychlé odkazy tel: a mailto: v detailu klientky
    • CTA Vytvořit rezervaci z detailu klientky a otevření /admin/.../rezervace?create=1&clientId=...
    • předvyplněný blok Vybraná klientka, fallback hlášku pro neplatné clientId a warning pro neaktivní klientku
    • vytvoření ruční rezervace pro předvyplněnou klientku bez obejití kolizní validace
    • vytvoření kolizního slotu odmítnuté serverem
    • editaci slotu s aktivní rezervací a blokaci neplatného snížení kapacity
    • archivaci pouze bez aktivní rezervace
  • Při úpravě lokálního dev serveru nebo next.config.ts ručně ověř i otevření aplikace z vedlejšího zařízení v LAN; pokud browser hlásí blokaci /_next/webpack-hmr, zkontroluj allowedDevOrigins a restartuj dev server.

  • Pokud /admin/sluzby v devu běží desítky sekund a browser hlásí ChunkLoadError na /_next/static/chunks/..., nejdřív ověř DB query stopu této stránky: list view nemá bez serviceId načítat detail služby, seznam má číst jen nezbytné sloupce a stavové počty musí jít přes jeden agregační dotaz (groupBy) místo více count.

  • Pokud /vouchery v devu po prvním pomalém renderu začne v browseru hlásit ChunkLoadError nebo Failed to fetch RSC payload, ověř, že route netahá celý veřejný katalog jen kvůli několika kartám doporučených služeb; landing page má používat limitovaný helper getVoucherSuggestedServices(3), ne getPublicServices().

  • Po změně media vrstvy ručně ověř i:

    • vytvoření upload rootu v MEDIA_STORAGE_ROOT
    • úspěšné uložení veřejného obrázku a jeho dostupnost přes /media/public/<kind>/... nebo legacy /media/<kind>/...
    • odmítnutí nepodporovaného MIME typu a příliš velkého souboru
    • smazání assetu v DB i na filesystemu
  • Po změně certifikátového workflow ručně ověř i:

    • /admin/media a /admin/provoz/media na desktopu i mobilu
    • tabs Vše / Certifikáty / Prostory / Kontakt / Portrét Homepage / Portrét O mně včetně správných počtů
    • oddělený portrét pro homepage a /o-mne bez legacy fallbacku
    • quick akci Publikovat / Skrýt přímo na kartě a návrat do stejného filtru
    • empty state pro prázdnou knihovnu i prázdný konkrétní filtr
    • upload certifikátu a okamžité propsání do /o-mne
    • smazání certifikátu a zmizení z adminu i veřejné stránky

Bezpečnost

  • Tajné údaje držet pouze v env.
  • ADMIN_SESSION_SECRET musí mít minimálně 32 znaků.
  • Admin routy nikdy neodemykat jen na základě klientského stavu.
  • Cancel/reschedule tokeny generovat jako náhodné tajné hodnoty, do DB ukládat pouze jejich hash.
  • Self-service reschedule nikdy nesmí přijímat veřejné bookingId; jediná veřejná cesta k rezervaci je přes validní RESCHEDULE token a server-side znovuověření stavu i časového limitu.
  • E-mail a telefon normalizovat na vstupu server-side ještě před zápisem do Client a Booking.
  • Telefon klientky ukládej jen přes sdílené helpery v src/features/booking/lib/client-phone.ts: prázdná hodnota je povolená, českých 9 číslic se normalizuje na +420..., prefix 00 na +, explicitní mezinárodní předvolba se respektuje a raw vstup s písmeny/HTML nesmí projít ani přes server action, ani přes booking engine.
  • Při veřejné rezervaci zamknout slot v transakci a znovu ověřit kapacitu až těsně před vytvořením Booking.
  • Při serializable konfliktu nebo deadlocku booking flow transakci krátce retryne místo okamžitého pádu na generickou chybu.
  • Server-side validace musí znovu ověřit i to, že délka vybrané služby reálně odpovídá délce slotu.
  • Pro anti-spam ochranu zapisuj submission logy i pro blokované pokusy, aby šlo dělat provozní audit bez zbytečného přidávání další infrastruktury.
  • E-mailové šablony drž jako čisté funkce bez přímé DB závislosti, aby šly jednoduše unit testovat.
  • Background worker je provozní proces, ne web request; při nasazení musí běžet odděleně od Next.js serveru.

Poznámky k releasu

  • Release checklist.
  • Migrační kroky (pokud jsou potřeba).

Týdenní plánování slotů

  • Hlavní workflow běží na /admin/volne-terminy a /admin/provoz/volne-terminy.
  • Mobilní FullCalendar nabízí pohled pro den, pracovní dny a víkend bez druhé implementace planneru.
  • [slotId] a [slotId]/upravit zůstávají kvůli kompatibilitě URL a přesměrují do správného týdne; samostatná route novy už neexistuje.
  • Mřížka používá 28 půlhodinových buněk na den (okno 06:00-20:00).
  • Výpočet začátku týdne musí vycházet z lokálního kalendářního dne Europe/Prague (pondělí jako první den), ne z getUTCDay() nad UTC půlnocí.
  • createdByUserId při planner mutacích ber z reálného AdminUser.id; bootstrap session identifikátory (bootstrap-owner, bootstrap-staff) nejsou DB FK a musí fallbacknout na null.
  • Seznam Volná okna v inspektoru dne neklíčuj jen přes startCell-endCell; po legacy fragmentaci se mohou objevit duplicitní rozsahy. Použij klíč navázaný i na dateKey a pořadí položky, aby planner v browseru negeneroval React warning o duplicitních keys.
  • Zápis do DB probíhá přes merge/split logiku:
    • prázdné nebo zelené buňky se z klienta pošlou jako rozsah buněk
    • server z nich spočítá časové hranice v časové zóně Europe/Prague
    • den znovu načte ze serveru
    • chráněné intervaly (rezervace, omezení služeb, neaktivní sloty, sloty s poznámkou nebo jinou kapacitou) odmítne měnit
    • zbylé jednoduché publikované sloty smaže a znovu založí jako minimální sadu souvislých intervalů
  • mutations.integration.test.ts má DB integrační scénář getAdminPlannerWeek(...), který ověřuje, že planner API vrací odděleně klientský čas služby (label), interní blok (blockedLabel, cleanupBlockedUntilLabel) i mapování cleanup overlay buněk (bookedCleanup) pro rezervace se snapshotem cleanupBlockMinutes.

Ruční QA pro planner

  • Ověř owner i salon variantu /admin/volne-terminy a /admin/provoz/volne-terminy.
  • Ověř přidání a odebrání jednoho půlhodinového bloku kliknutím bez horizontálního scrollu editoru dne.
  • Ověř přidání delšího úseku tažením a následné sloučení do jednoho AvailabilitySlot.
  • Ověř odebrání části dostupnosti ze zeleného bloku a správné rozdělení na zbylé intervaly.
  • Ověř průběžné uložení změny a obnovení uloženého stavu po chybě.
  • Ověř, že zásah do rezervace nebo omezeného slotu vrátí srozumitelnou chybu a nic nepřepíše.
  • Ověř, že slot s CANCELLED rezervací planner neukazuje jako blokaci a nezůstává v mřížce jako šedý nebo uzamčený historický stín.

Prague Time And DST

  • Salon-facing time is Europe/Prague; do not rely on server local timezone or fixed +01:00 / +02:00 offsets.
  • Planner slot creation should use dateKey + half-hour cell index through getCellRangeBounds(...).
  • Public booking catalog may return UTC ISO strings, but all client display helpers must render with timeZone: "Europe/Prague".
  • Regression tests for DST live in admin slot time helpers, booking formatting, public booking helpers, e-mail templates and ICS utilities.
  • Totéž platí i pro admin-side preview lokálně zadaného data/času: drawer Přidat rezervaci a Přesunout termín nesmí skládat preview přes browser-local new Date("YYYY-MM-DDTHH:mm"), ale přes resolvePragueLocalDateTime(...), jinak se uložený čas rozjede s UI mimo CZ timezone a kolem DST.
  • manualOverride je provozní výjimka jen pro explicitní ruční zadání termínu. Pokud admin vybírá konkrétní published slot a ten je stale nebo po změně služby už nevyhovuje, backend musí požadovat nový výběr slotu místo tichého vytvoření draft override slotu.
  • Při ruční rezervaci pro selectedClientId je prázdný e-mail ve formuláři jen „bez změny kontaktu“, ne pokyn ke smazání Client.email. Pokud potřebuješ kontakt vědomě vyčistit, dělej to přes samostatný klientský/contact edit flow.
  • Booking acquisition helper v booking-acquisition.ts mapuje do stejných polí i mtm_* query parametry; nové tracking integrace proto u kampaní preferuj psát tak, aby fungovaly s utm_* i mtm_* bez rozdílu v admin read modelu.