# Rejstřík znovupoužitelných funkcí a komponent K čemu je co, aby se za rok nepsalo znovu něco, co už existuje. Když píšeš druhou funkci, která dělá skoro totéž jako něco odsud, je to skoro vždycky chyba - buď se má použít ta původní, nebo se má rozšířit. Podrobný popis je vždycky v komentáři u samotné funkce. Tady je jen jedna věta a kdy to použít. ## Ukládání dat (server) Rozhoduje se na **jednom místě**, viz [14-databaze.md](14-databaze.md). Volající nikdy nezjišťuje, jestli běží Postgres, soubor, nebo pamět. | Co | Kde | K čemu | | ------------------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `defineStore(kind)` | `src/data/store/index.ts` | Založí úložiště pro nový druh záznamu. Jeden řádek na entitu. | | `initStores({databaseReady})` | `src/data/store/index.ts` | Vybere režim pro **všechna** úložiště včetně konektorů. Volá se jednou při startu, nikde jinde. | | `refreshCache(kind)`, `refreshAllCaches()` | `src/data/store/cached.ts` | Registr cache. Po zápisu obnovit jen tu entitu, ne všechny. | | `listByTenant(tenantIds, sortBy)` | `src/data/store/cached.ts` | Filtr na firmu plus řazení nad cache. Místo sedmi kopií `filter` + `sort` v modulech entit. | | `nowIso`, `minutesAgo`, `highestNumber`, `writableOrWarn` | `src/data/store/types.ts` | Drobnosti pro moduly úložišť: časová značka, čas před N minutami, nejvyšší číslo ID pro čítač, varování při zápisu do úložiště jen pro čtení. | | `mergeValues(current, patch)` | `src/data/connectors/types.ts` | Sloučení hodnot konektoru při `PATCH`: prázdný řetězec maže, chybějící klíč nechává. Jedna implementace pro soubor i Postgres. | | `flushStores()` | `src/data/store/index.ts` | Dopíše rozepsané zápisy. Jen při ukončení procesu. | | `withCache(store)` | `src/data/store/cached.ts` | Kopie v paměti pro **konfigurační** entity, které se čtou při každém requestu (uživatelé, role, firmy). Čte se synchronně, obnovuje se po zápisu. | | `withMirror(store)` | `src/data/store/mirror.ts` | Opačný směr než `withCache`: data se mění v paměti a po každé změně se celý záznam zapíše. Pro **provozní** data (tickety, automatizace, incidenty, rozložení). | | `isVisible(entity, options)` | `src/data/store/types.ts` | Vidí volající tenhle záznam? Prázdný seznam firem znamená "nic", ne "vše". | | `nowIso()` | `src/data/store/types.ts` | Časová značka. Ať se nepíše `new Date().toISOString()` na třiceti místech. | | `memorySnapshot` / `fileSnapshot` | `src/data/snapshot.ts` | Nižší vrstva pod `createLocalStore`: atomický zápis JSONu s debounce. Přímo se nepoužívá. | | `db()`, `query`, `queryOne`, `transaction` | `src/db/pool.ts` | Postgres. `dbFor(tenantId)` je připravený šev pro rozdělení na víc databází. | | `seal`, `open`, `sealAll`, `openAll` | `src/db/secretBox.ts` | Šifrování přístupových údajů konektorů (AES-256-GCM). Nic tajného se neukládá jinak. | | `runMigrations()` | `src/db/migrate.ts` | Migrace pod zámkem, jeden soubor = jedna transakce. | ## Entity a práva (server) | Co | Kde | K čemu | | ---------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `crudRouter(options)` | `src/routes/crud.ts` | Celý CRUD nad jednou entitou: seznam, detail, vytvoření, úprava, mazání, právo, audit. Nová entita v nastavení = jeden `crudRouter`, ne pět handlerů. | | `readScope(req)` | `src/routes/crud.ts` | Ze které firmy smí request číst. Povinný argument všech `list` volání. | | `accessFor(user, tenantId?)` | `src/data/access.ts` | Co uživatel smí: práva, záložky, výchozí firma. Klient si nic nedovozuje sám. | | `permissionsOf(user, tenantId)` | `src/data/permissions.ts` | Efektivní práva z rolí. Pětisekundová cache, `invalidatePermissions()` po zápisu. | | `hasPermission(...)` | `src/data/permissions.ts` | Jedna kontrola. Používá ji `crudRouter` i ruční handlery. | | `navFor(...)` | `src/data/tenantFeatures.ts` | Průnik toho, co firma má, a toho, na co má člověk právo. Navigace chodí ze serveru. | | `recordAudit(input)` | `src/data/audit.ts` | Zápis do auditu. Nevrací chybu a nečeká se - rozbitý audit nesmí rozbít aplikaci. | | `enqueue(input)` | `src/runtime/queue.ts` | Zařadí běh. Klíč proti dvojímu zařazení drží jeden běh na jednu událost. | | `claimBatch(limit, active)` | `src/runtime/queue.ts` | Vezme další práci, spravedlivě po firmách, přeskočí to, co už běží. Místo, kde nad Postgresem musí být SKIP LOCKED. | | `touchClaim(item)` | `src/runtime/queue.ts` | Tlukot běžícího běhu. Bez něj se dlouhý běh po 30 minutách považuje za zaseknutý a vykoná se podruhé. | | `findOpenIncident(source, tenantIds)` | `src/data/incidentStore.ts` | Otevřený incident téže příčiny v téže firmě. Stejná chyba nezakládá druhý. | | `attachAccess`, `tenantOrDeny`, `optionalTenantOrDeny`, `scopeOrDeny` | `src/middleware/tenant.ts` | Přístup jednou na request do `req.access`, firma requestu z něj. Route si to nepočítá sama. | | `safeRouter()`, `wrap(handler)` | `src/middleware/asyncHandler.ts` | Router, ve kterém odmítnutá promise skončí jako 500 s logem. Každá nová route vzniká tady. | | `rateLimit({name, windowMs, max})` | `src/middleware/rateLimit.ts` | Limit requestů v paměti, 429 s `Retry-After`. Jen na veřejných endpointech, kde se dá hádat. | | `validationError(res, zodError, message?)` | `src/middleware/validation.ts` | Jeden tvar chyby validace: z chyby zodu udělá `issues` po polích. | | `timingSafeEqualString(a, b)` | `src/lib/secure.ts` | Porovnání tokenu v konstantním čase. Webhook, příjem, pozvánky. | | `assertAllowedUrl`, `readBodyLimited`, `readJsonLimited`, `describeFetchError` | `src/net/guard.ts` | Vše, co volá ven: zákaz vnitřní sítě, čtení těla proudem s limitem, čitelný popis chyby sítě. HTTP skriptů, MCP, SMTP i ARES. | | `publishOutputs(...)` | `src/runtime/executor.ts` | Zápis výstupů kroku do kontextu, jednou pro vestavěné kroky i skripty. Holé jméno nepřepíše, co už v kontextu je. | | `lookupCompany`, `searchCompanies`, `listCompanyPersons`, `placeholderEmail`, `isPlaceholderEmail` | `src/ares/client.ts` | Registr ARES: firma podle IČ nebo názvu, statutáři, zástupná adresa a její rozpoznání. | | `onTicketEvent(kind, ticket)` | `src/runtime/triggers.ts` | Změna ticketu zařadí navázané automatizace, včetně ochrany proti smyčce. | | `withRun(marker, work)` | `src/runtime/context.ts` | Označí, který běh práci způsobil. Bez toho automatizace spouští sama sebe. | | `findBuiltinStep(...)` | `src/runtime/builtinSteps.ts` | Kroky, které sahají do našeho úložiště, ne ven přes HTTP. | | `personView(user, membership)` | `src/data/people.ts` | Jediné místo, kde z účtu a jednoho členství vzniká pohled `Person`. Řešitel je členství, ID řešitele je ID účtu. | | `listPeople(tenantIds)`, `listAllPeople(tenantIds)` | `src/data/people.ts` | Řešitelé vybraných firem, jeden záznam na členství; druhá i s vypnutými účty (správa týmu). | | `findPerson(id, tenantId)` | `src/data/people.ts` | Řešitel v dané firmě, firma je povinná. Kdo v ní není členem, je `undefined`, i když účet existuje. | | `personName(id)` | `src/data/people.ts` | Jméno účtu bez ohledu na firmu, pro popisky u záznamů, které už prošly filtrem na firmu. | | `personIdFor(user, tenantId)` | `src/data/people.ts` | ID řešitele, kterým je uživatel ve firmě: ID účtu při členství, jinak `null`. Neptat se `user.id` přímo. | | `findPersonByExternalId(value, tenantIds)` | `src/data/people.ts` | Řešitel podle ID z cizí aplikace, například voicebotId. Externí ID visí na členství. | | `migratePeople()` | `src/data/migratePeople.ts` | Jednorázový převod starých záznamů řešitelů (`ppl_`) na účty při startu. Přepisuje odkazy přes `remapPersonIds` v `ticketStore.ts` a `automationStore.ts`. | | `notify(input)` | `src/data/notifications.ts` | Upozorní člověka. Nečeká se a nevyhazuje chyby, stejně jako audit. | | `runFlow(steps, context, options)` | `src/runtime/executor.ts` | Vykoná strom kroků. Nikdy nevyhodí výjimku, chyba je výsledek. Používá to akce na ticketu i webhook, aby se strom choval všude stejně. | | `widgetCatalog(tenantIds, userId)` | `src/data/widgets.ts` | Jediná definice toho, co jde položit na dashboard. Používá ji nabídka i kontrola ukládaného rozložení. | | `intakeEvent(input)` | `src/data/ticketStore.ts` | Přijme událost zvenku: podle externího ID buď založí ticket, nebo ji navěsí na existující. Jediná cesta, kterou se událost stává ticketem. Hodnoty z `input.apply` zapíše v obou případech, prázdné nemaže. | | `getAgentStats(...)` | `src/data/ticketStore.ts` | Výkon řešitelů: odbavené, mediány časů, vrácené, fronta. Používá to widget i detail osoby, aby čísla seděla. | | `findByExternalId(...)` | `src/data/ticketStore.ts` | Ticket firmy podle externího ID. Klíč je dvojice firma a ID. | | `findByIntakeToken(token)` | `src/data/tenants.ts` | Firma podle tokenu příjmu. Určuje i to, v jakém rozsahu je externí ID unikátní. | | `refreshCaches()`, `refreshEntity(kind)` | `src/data/bootstrap.ts` | Obnoví všechny kopie v paměti, nebo jen jednu entitu. Route nastavení volá `bootstrapDataRefresh(route)` v `src/data/refresh.ts`, která vybere tu jednu. | | `bootstrapData({databaseReady})` | `src/data/bootstrap.ts` | Seznam všech entit a provozních dat. **Nová entita se přidává tady**, ne rozesetě po modulech. | ## Skripty a konektory (server) Viz [11-skripty-konektoru.md](11-skripty-konektoru.md). | Co | Kde | K čemu | | ------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `runScript(id, inputs, ctx)` | `src/scripts/runner.ts` | Spustí skript. **Nikdy nevyhodí výjimku**, chybu vrací jako výsledek s celým hlášením. | | `validateValues(...)` | `src/scripts/values.ts` | Jedna kontrola pro vstupy i výstupy skriptu podle manifestu. | | `scriptUtil` | `src/scripts/util.ts` | Nádobíčko pro skripty: `pick`, `first`, `num`, `date`, `day`, `list`, `addresses`, `quote`, `need`, `get`, `applyRules`, `fillJson`. Skript nemá sahat na nic jiného. | | `pick`, `pickText`, `jwtExpiry`, `parseBool`, `parseNumber` | `src/scripts/util.ts` | Totéž pro server: pole bez ohledu na velikost písmen, `exp` z JWT, převody. Než napíšeš `Number(x)` s kontrolou `NaN`, je to tady. | | `DETAIL_BYTES`, `truncate(value)` | `src/scripts/util.ts` | Jeden limit na zkracování detailu chyby pro všechny vrstvy. Žádné vlastní `slice(0, 600)`. | | `createRedactor(...)` | `src/scripts/util.ts` | Vyškrtá tajemství z textu **před** logováním, i v URL-encoded a JSON-escaped tvaru. Používá se u všeho, co jde do logu. | | `scriptConfig(target)` | `src/scripts/connections.ts` | Nastavení napojení bez tajných polí. Jediné, co skript dostane jako `ctx.config`. | | `applyRules`, `fillJson` | `src/scripts/mapping.ts` | Transformace dat: pole na pole s převody, nebo objekt na objekt. Viz [13-transformace-dat.md](13-transformace-dat.md). | | `getPath(obj, path)` | `src/scripts/mapping.ts` | Čtení `zakaznik.adresa.mesto` z neznámého objektu. | | `resolveTarget(...)` | `src/scripts/connections.ts` | Z konektoru poskládá adresu a hlavičky. Přístupové údaje nikam jinam nevedou. | | `serviceBaseUrl(service)` | `src/scripts/connections.ts` | Adresa služby: naše aplikace ze `SERVICES_BASE_URL`, cizí (OpenAI) z jejího `baseUrl`. Přebít jde přes `_BASE_URL`. | | `targetSecrets(target)` | `src/scripts/connections.ts` | Co se musí vyškrtat z logu. Vrací i holý klíč bez předpony `Bearer `, protože v něm ho cizí služby vracejí v chybách. | | `createHttp(...)` | `src/scripts/http.ts` | HTTP se timeoutem, limitem odpovědi a rozlišením "zkusit znovu" a "marné". | | `isPrivateHost(host)` | `src/scripts/http.ts` | Míří jméno do vnitřní sítě? Jedno pravidlo pro HTTP i pro SMTP server z konektoru. | | `sendMail(target, message)` | `src/mail/smtp.ts` | Odešle e-mail přes SMTP z konektoru. Nikdy nevyhodí výjimku, vrací i to, jestli má smysl zkusit znovu. | | `verifySmtp(target)` | `src/mail/smtp.ts` | Přihlásí se na server bez odeslání zprávy. Tím se ověřuje konektor e-mailu. | | `escapeHtml(value)` | `src/data/templates.ts` | Escapuje **dosazenou hodnotu** v HTML šabloně. Značky autora šablony zůstávají, ostré závorky od zákazníka ne. | | `ctx.http.postForm(...)` | `src/scripts/http.ts` | Odeslání souboru (`multipart/form-data`). Obsah přichází jako Base64, hranici dopisuje runtime. | | `scriptIdFor(serviceId, operationId)` | `src/scripts/lookup.ts` | Který skript obsluhuje operaci z katalogu. | ## Klient | Co | Kde | K čemu | | --------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `EntityAdmin` | `components/dashboard/EntityAdmin.tsx` | Celá správa jedné entity: tabulka, modál, validace, mazání. Nová záložka nastavení = popis sloupců a polí, ne nová stránka. | | `parseJsonField` | `components/dashboard/EntityAdmin.tsx` | Textové pole s JSONem na hodnotu, s hlášením, kde je chyba. | | `TicketActions` | `components/dashboard/TicketActions.tsx` | CTA akcí na ticketu plus typ, tagy a vlastní pole. Seznam akcí chodí ze serveru už vyfiltrovaný. | | `ErrorDetail` | `components/dashboard/ErrorDetail.tsx` | Rozbalovací celé chybové hlášení s kopírováním. Chyba se nikdy nezkracuje. | | `CustomWidgetCard` | `components/dashboard/widgets/CustomWidget.tsx` | Vykreslí widget, jehož data počítá server: číslo, pruhy, tabulka výkonu, časová řada, seznam, data z konektoru. | | `TicketTable` | `components/dashboard/TicketTable.tsx` | Tabulka ticketů pro všechna místa. Na mobilu se místo posouvání do strany kreslí karty. | | `TicketEvents` | `components/dashboard/TicketEvents.tsx` | Příchozí události ticketu včetně celého přijatého JSONu. | | `ViewSwitch` | `components/dashboard/ViewSwitch.tsx` | Přepínač tabulka nebo dlaždice. Používají ho všechny seznamy. | | `FlowCanvas` s `start` | `components/dashboard/flow/FlowCanvas.tsx` | Tentýž strom kroků i bez spouštěče - pro tělo akce, které spouští člověk. | | `MappingEditor` | `components/dashboard/flow/MappingEditor.tsx` | Editor transformací v obou režimech (pole na pole, JSON). | | `DataState` | `components/dashboard/DataState.tsx` | Načítání, chyba, prázdno. Ať to každá stránka nekreslí po svém. | | `apiFetch` | `lib/api.ts` | Jediná cesta na API: base path, token, `ApiError` s celým hlášením ze serveru. | | `useApiQuery` | `lib/useApiQuery.ts` | Načtení dat do stránky včetně `reload`, `refreshing` a `total`. S `body` pošle POST, s `enabled: false` se neptá, `patchOn` opraví data z události bez dotazu. | | `useCollection(key)`, `useAccess()`, `useCollectionSelector` | `lib/collections.tsx` | Číselníky za firmu (lidé, skupiny, typy, služby, konektory, přístup) ze sdíleného skladu, opravované z událostí. Ne `apiFetch` na číselník ze stránky. | | `patchTicketList(...)` | `lib/ticketEvents.ts` | Oprava seznamu ticketů z `payload.ticket` v události. Použít jako `patchOn`. | | `apiFetchWithMeta` | `lib/api.ts` | Jako `apiFetch`, ale vrací i `X-Total-Count`. Pro stránkované seznamy. | | `useSubmit(fn)` | `lib/useSubmit.ts` | Odeslání formuláře: `saving`, chyba, reset. Dvanáct řádků, které si dřív psal každý formulář zvlášť. | | `useUnsavedChanges(dirty)` | `lib/useUnsavedChanges.ts` | Varování při odchodu z rozepsaného formuláře nebo stromu. | | `priorities`, `priorityLabel` | `lib/options.ts` | Pevné číselníky. Stavy a kanály se berou ze serveru (`/widget-data/options`). | | `plural(count, forms)` | `lib/format.ts` | Skloňování počtu (1 ticket, 2 tickety, 5 ticketů). | | `Field`, `Input`, `Select`, `Textarea` | `components/ui/form/` | Formulářové prvky s jednou sadou tříd (`controlClass`). Vlastní `inputClass` ve stránce je chyba. | | `Chip` | `components/ui/Chip.tsx` | Štítek. | | `TicketCard` | `components/dashboard/TicketCard.tsx` | Karta ticketu pro dlaždice a mobil, varianta `compact` pro widgety. | | `useMediaQuery(query)` | `lib/useMediaQuery.ts` | Tabulka nebo karty podle šířky. `TicketTable` podle toho kreslí obojí, druhá komponenta není. | | `cn(...)` | `lib/cn.ts` | Skládání tříd. Podmíněné třídy nikdy ručně přes šablonu. | | `format*` | `lib/format.ts` | Čísla, procenta, datum, relativní čas, trvání. Formátování se nepíše v komponentě. | | `serviceIcon(key)` | `lib/serviceIcons.ts` | Klíč ikony ze serveru na komponentu. Server neposílá komponenty. | | `usePageMeta` | `lib/usePageMeta.ts` | Titulek stránky. | | `Badge`, `Button`, `Modal`, `Card`, ... | `components/ui/` | Základní prvky. Nový vzhled tlačítka patří sem, ne do stránky. | ## Pravidla, která z toho plynou 1. **Nová entita v nastavení**: `defineStore` v modulu entity, řádek v `bootstrap.ts`, `crudRouter` v `settings.ts`, popis v `Settings.tsx`. Nic jiného se psát nemusí. 2. **Data, která se mění za provozu**, jdou přes `withMirror`. Data, která se čtou při každém requestu a mění zřídka, přes `withCache`. Obojí nikdy. 3. **Chybu se nesmí zkracovat.** Server vrací celé hlášení, klient ho umí zobrazit (`ErrorDetail`). 4. **Klient nepočítá práva.** Co smí, říká `accessFor`.