Files
csbot-prototype/documentation/15-rejstrik-funkci.md
T
JiriUhlirandClaude Fable 5.1 ea9387bea9 Seed z repozitare, zatezove testy, worker bez spanku, dialogy bez rozmazani
Nastaveni prezije nasazeni: seed/records.json se pri prazdnem ulozisti
nacte misto ukazkovych dat (zive uloziste se nikdy neprepisuje). Soubor
nese soucasny stav produkce (firmy s provozovatelem, role, typ ticketu,
akce, widgety, skupiny, rozlozeni). Novy GET /api/admin/export a skript
npm run seed:export pro dalsi exporty, Dockerfile slozku kopiruje.

Vykonnostni testy (npm run test:perf) nad 200 firmami a 10 000 tickety
a zatezovy skript (npm run load) proti bezici instanci vcetne davky
udalosti na webhook. Mereni odhalilo strop workeru: po obsazeni vsech
mist spal sekundu, takze fronta odbavila nejvys 4 behy za sekundu. Ted
ceka na prvni dokonceny beh: 500 udalosti za 1,3 s (395 behu/s). Strop
posluchacu streamu zvednut na 2 000.

Dialogy: prekryv modalu a menu v portalu bez backdrop-blur, tecka Zive
pulzuje jen pri navazovani spojeni - rozmazani cele obrazovky pod trvalou
animaci sekalo video vedle portalu. Bublina udalosti drzi 0,5 s.

Dokumentace 14, 19, 20, 22, 04, 01, 03, 15 a 99 aktualizovana.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 20:41:42 +02:00

36 KiB

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. Volající nikdy nezjišťuje, jestli běží Postgres, soubor, nebo pamět.

Co Kde K čemu
defineStore<T>(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.
storeByKind(kind) src/data/store/index.ts Uloziste podle druhu z registru. Jen pro export nastaveni; modul entity si bere svoje uloziste primo.
seedFromFile(kind, seed?, file?) src/data/store/seedFile.ts Seed pro prazdne uloziste: ze seed/records.json, kdyz druh obsahuje, jinak z kodu. Vola defineStore v init, jinde se nevola.
loadSeedFile(file), SEED_KINDS, isSeedKind src/data/store/seedFile.ts Nacteni a kontrola souboru (jednou za cestu, zod na obalku a id), seznam konfiguracnich druhu. SEED_KINDS je jediny seznam pro import i export.
exportSeed() src/data/seedExport.ts Vsechny zaznamy konfiguracnich druhu z ulozist ve tvaru seed/records.json. Pouziva GET /api/admin/export.
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 (<SLUZBA>_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/<skupina>.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
operatorTenant() src/data/tenants.ts
clearOtherOperators(keepId) src/data/tenants.ts
checkUploads(uploads, existingCount) src/data/attachments.ts
addAttachments(ticket, uploads, uploadedBy) src/data/attachments.ts
listAttachments, getAttachment, removeAttachment src/data/attachments.ts
sanitizeName, normalizeMime, decodeBase64, formatBytes src/data/attachments.ts
jsonLimitFor(count, maxBytes), hasOwnBodyLimit(path) src/routes/bodyLimit.ts
visibleTicketOrDeny(req, res) src/routes/ticketActions.ts
brandOfOperator() src/routes/public.ts
refreshCaches(), refreshEntity(kind) src/data/bootstrap.ts
bootstrapData({databaseReady}) src/data/bootstrap.ts

Skripty a konektory (server)

Viz 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.
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 <SLUZBA>_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<T> lib/api.ts Jediná cesta na API: base path, token, ApiError s celým hlášením ze serveru.
useApiQuery<T> 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<T> 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 <input type="file">, 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/<entita>.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.