# 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 `tickets/remap.ts` a `automations/remap.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/tickets/intake.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/tickets/stats.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/tickets/queries.ts` | Ticket firmy podle externího ID. Klíč je dvojice firma a ID. | | `createApp()` | `src/app.ts` | Sestavi Express aplikaci: middleware, routery, health, Swagger, statika, SPA. Bez `listen` a bez nacteni dat, takze jde postavit v testu (supertest). `index.ts` ji jen spusti. | | `config.serviceBaseUrlOverride(variable)` | `src/config.ts` | Jedine cteni `process.env` s dynamickym nazvem (`_BASE_URL`). Vraci normalizovanou adresu nebo `null`. Nikde jinde se `process.env` necte. | | `buildOpenApiDocument()` | `src/openapi/index.ts` | Sklada OpenAPI z `components.ts` a `paths/*.ts`. Novy endpoint se popisuje v souboru sveho routeru, `crudPaths` v `helpers.ts` popise CRUD petici jednim radkem. | | `pageFrom(query)`, `paginate(items, page)` | `src/routes/dashboard/shared.ts` | Strankovani seznamu ticketu a fronty behu: `limit`, `offset`, strop `MAX_PAGE_LIMIT`. Nepsat vlastni `slice` v route. | | `memberOf(user, tenantId)` | `src/routes/settings/shared.ts` | Je ucet clenem firmy? Sdili sprava uctu a resitelu. | ### Moduly dat po rozdeleni Puvodni soubory zustavaji jako fasady (`ticketStore.ts`, `automationStore.ts`, `services.ts`), importy se nemeni. Kdo hleda, kde co je: | Modul | Co drzi | | ---------------------------------- | ------------------------------------------------------------------------------------------- | | `tickets/index.ts` | verejne API slozky, seed a `initTickets` v poradi, ve kterem se maji volat | | `tickets/model.ts` | `StoredTicket`, `defaultStatuses`, `channelLabels`, `toTicket` (doplneni vychozich hodnot) | | `tickets/state.ts` | pole ticketu, indexy podle ID a externiho ID, log, udalosti, citace ID | | `tickets/persist.ts` | `persist`, `touch` (`updatedAt` a zapis v jednom), `initTickets` | | `tickets/queries.ts` | `listTickets` s `TicketFilter`, `getTicket`, `findTicket`, `findByExternalId`, `ticketWithinVisibility` | | `tickets/store.ts` | zapisy: `createTicket`, `updateTicketStatus`, `assignTicket`, `setTicketType`, `setTicketTags`, `assignTicketGroup`, `claimTicket`, `addComment`, `noteAttachment` (radek v logu a `ticket.updated` po zmene prilohy) | | `tickets/intake.ts` | `intakeEvent`: udalost zvenku se stane ticketem nebo se navesi | | `tickets/trace.ts` | `appendTrace`, `flattenTrace`, `lastTraceId`, `describePayload`: log prubehu | | `tickets/stats.ts` | `getWorkload`, `getAgentStats` | | `tickets/seed.ts`, `remap.ts` | ukazkova data (`SEED_DEMO=1`), preznaceni resitelu pri migraci | | `automations/index.ts` | verejne API slozky, seed a `initAutomations` | | `automations/model.ts` | `StoredAutomation`, `rulesOf`, `matchOf` (cteni podminky ve stare i nove podobe) | | `automations/state.ts` | `Map` automatizaci, `nextId`, `findWritable` | | `automations/persist.ts` | `save`, `initAutomations`, `mirror` | | `automations/store.ts` | `listAutomations`, `getAutomation`, `createAutomation`, `updateAutomation`, `regenerateWebhookToken`, `findByWebhookToken`, `recordRun`, `deleteAutomation` | | `automations/validation.ts` | `countSteps`, `collectFlowIssues`, `deriveKind`, `withDerived`: ciste funkce nad stromem | | `automations/webhook.ts` | `generateWebhookToken`, `withWebhookToken`, `recordWebhookCall`, `recentWebhookCalls`, `bodyForCall` | | `automations/runs.ts` | historie behu po dnech, `KEEP_DAYS`, `statsOf` | | `automations/seed.ts`, `seedDemo.ts`, `remap.ts` | skutecne automatizace (vzdy), ukazkove (`SEED_DEMO=1`), preznaceni resitelu | | `services/index.ts` | katalog za behu: `findService`, `findOperation`, `actionsFor`, `serviceCatalog`, `setScriptActions`, `setMcpOperations`, `withRuntimeOptions`, `visibleServices` | | `services/catalog/index.ts` | `services` a `serviceCategories` slozene ze skupin; poradi tady je poradi v nabidce | | `services/catalog/.ts` | staticky zapis sluzeb jedne skupiny: `triggers`, `incident`, `ticket`, `crm`, `finance`, `logistics`, `email`, `messaging`, `social`, `office`, `analytics`, `ai`, `mcp`, `tools`, `polstryn` | | `findByIntakeToken(token)` | `src/data/tenants.ts` | Firma podle tokenu příjmu. Určuje i to, v jakém rozsahu je externí ID unikátní. | | `operatorTenant()` | `src/data/tenants.ts` | Zapnuta firma s priznakem provozovatele portalu. Komu chodi poptavky z webu a odkud web bere udaje. `undefined` = neni nastaven. | | `clearOtherOperators(keepId)` | `src/data/tenants.ts` | Sunda priznak provozovatele vsem ostatnim firmam a ohlasi je do streamu. Vola `afterWrite` CRUD firem, provozovatel je vzdy jen jeden. | | `checkUploads(uploads, existingCount)` | `src/data/attachments.ts` | Cista kontrola davky souboru: pocet, base64, velikost, nazev. Volat pred zapisem, kontakt ji vola pred zalozenim ticketu. | | `addAttachments(ticket, uploads, uploadedBy)` | `src/data/attachments.ts` | Ulozi soubory k ticketu a kazdy zapise do logu. Cela davka projde, nebo nic. | | `listAttachments`, `getAttachment`, `removeAttachment` | `src/data/attachments.ts` | Seznam bez obsahu, jedna priloha s obsahem, smazani se zapisem do logu. Vzdy s `ticketId` a `tenantIds`, cizi je jako neexistujici. | | `sanitizeName`, `normalizeMime`, `decodeBase64`, `formatBytes` | `src/data/attachments.ts` | Nazev bez cesty a ridicich znaku, typ obsahu do hlavicky, prisne base64, velikost pro cloveka. | | `jsonLimitFor(count, maxBytes)`, `hasOwnBodyLimit(path)` | `src/routes/bodyLimit.ts` | Strop tela pro routy se soubory v base64 a seznam cest, ktere globalni `express.json` preskakuje. | | `visibleTicketOrDeny(req, res)` | `src/routes/ticketActions.ts` | Ticket z `:id` pres strop viditelnosti za firmu ticketu, jinak 404. Pouzivaji vestavene akce i prilohy. | | `brandOfOperator()` | `src/routes/public.ts` | `PublicBrand` z provozovatele portalu, same `null` bez nej. | | `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/runtime/scripts/runner.ts` | Spustí skript. **Nikdy nevyhodí výjimku**, chybu vrací jako výsledek s celým hlášením. | | `validateValues(...)` | `src/runtime/scripts/values.ts` | Jedna kontrola pro vstupy i výstupy skriptu podle manifestu. | | `scriptUtil` | `src/runtime/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/runtime/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/runtime/scripts/util.ts` | Jeden limit na zkracování detailu chyby pro všechny vrstvy. Žádné vlastní `slice(0, 600)`. | | `createRedactor(...)` | `src/runtime/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/runtime/scripts/connections.ts` | Nastavení napojení bez tajných polí. Jediné, co skript dostane jako `ctx.config`. | | `applyRules`, `fillJson` | `src/runtime/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/runtime/scripts/mapping.ts` | Čtení `zakaznik.adresa.mesto` z neznámého objektu. | | `resolveTarget(...)` | `src/runtime/scripts/connections.ts` | Z konektoru poskládá adresu a hlavičky. Přístupové údaje nikam jinam nevedou. | | `serviceBaseUrl(service)` | `src/runtime/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/runtime/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/runtime/scripts/http.ts` | HTTP se timeoutem, limitem odpovědi a rozlišením "zkusit znovu" a "marné". | | `isPrivateHost(host)` | `src/runtime/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/runtime/scripts/http.ts` | Odeslání souboru (`multipart/form-data`). Obsah přichází jako Base64, hranici dopisuje runtime. | | `scriptIdFor(serviceId, operationId)` | `src/runtime/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. | | `TicketAttachments` | `components/dashboard/TicketAttachments.tsx` | Sekce priloh v detailu ticketu: seznam, stazeni pres fetch s tokenem, nahrani, smazani. Vlastni dotaz, obnovi se z `ticket.updated`. | | `TicketAssignPanel` | `components/dashboard/TicketAssignPanel.tsx` | Panel resitele a skupiny v detailu ticketu, ciselniky si bere sam. Vyclenen z `TicketDetail`. | | `TenantsAdmin` | `components/dashboard/settings/TenantsAdmin.tsx` | Sprava firem v Nastaveni vcetne priznaku provozovatele a kontaktnich udaju. Vyclenena ze `Settings.tsx`. | | `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` | `hooks/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)` | `hooks/useSubmit.ts` | Odeslání formuláře: `saving`, chyba, reset. Dvanáct řádků, které si dřív psal každý formulář zvlášť. | | `useUnsavedChanges(dirty)` | `hooks/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. | | `FilePicker` | `components/ui/form/FilePicker.tsx` | Vyber priloh: tlacitko, skryty ``, seznam vybranych, kontrola poctu a velikosti pri vyberu. Soubory drzi rodic. | | `readFileAsBase64`, `validateFiles`, `formatBytes`, `MAX_ATTACHMENT_BYTES` | `lib/files.ts` | Soubor na base64 bez prefixu `data:`, kontrola davky proti limitum, velikost pro cloveka. Limity stejne jako na serveru. | | `apiBlob(path)` | `lib/api.ts` | Stazeni binarniho obsahu s hlavickou `Authorization`. Obycejny odkaz token neposle. | | `useBrand()` | `hooks/useBrand.ts` | Udaje provozovatele z `/api/public/brand` nad statickou zalohou `config/brand.ts`. Jeden dotaz na nacteni stranky, sdileny. | | `Chip` | `components/ui/Chip.tsx` | Štítek. | | `Table`, `TableHead`, `Th`, `TableRow`, `Td` | `components/ui/Table.tsx` | Tabulka seznamu v portalu (hlavicka verzalkami, radky s linkou). Ctyri stranky ji kreslily kazda jinak. Huste tabulky ticketu a vykonu zustavaji zvlast, jsou to jine tabulky. | | `ServiceIcon` | `components/ui/ServiceIcon.tsx` | Ikona sluzby podle klice z katalogu. Misto `const Icon = serviceIcon(key)` v JSX, ktere lint hlasi jako komponentu vytvorenou pri vykresleni. | | `EntityForm` | `components/dashboard/EntityForm.tsx` | Formular jedne entity v modalu, pouziva ho `EntityAdmin`. Pole z popisu sloupcu, hodnoty a chyby v propsech. | | `useLatest(value)` | `hooks/useLatest.ts` | Ref s posledni hodnotou pro callbacky, ktere nemaji byt v zavislostech effectu. Zapis v layout effectu, aby vykresleni zustalo ciste. | | `useSyncFromSource(source, apply)` | `hooks/useSyncFromSource.ts` | Prevzeti dat ze zdroje do rozepsaneho stavu uz pri vykresleni, ne v `useEffect`. Stara kopie neproblikne a stranka se nekresli dvakrat. | | `TicketCard` | `components/dashboard/TicketCard.tsx` | Karta ticketu pro dlaždice a mobil, varianta `compact` pro widgety. | | `useMediaQuery(query)`, `MD_UP` | `hooks/useMediaQuery.ts` | Tabulka nebo karty podle šířky pres `useSyncExternalStore`, prohlizec je zdroj pravdy. `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` | `hooks/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 `src/routes/settings/.ts` a mount v `settings/index.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`.