Files
csbot-prototype/documentation/15-rejstrik-funkci.md
T
JiriUhlirandClaude Opus 5 1134852bff Zalozit nebo doplnit ticket: doplneni konecne doplnuje
Krok mel v poli Obsah nastaveno {{rating}}. Data v behu prokazatelne byla,
v udalostech ticketu je hodnoceni videt cele, ale ticket zustal s prazdnym
obsahem.

intakeEvent deli praci na zalozeni a navazani na existujici ticket a vsechno
z `create` platilo jen pro tu prvni vetev. U existujiciho ticketu se doplnovaly
pouze vlastni pole a stitky, zbytek se tise zahodil. U hovoru to znamena, ze
obsah nedorazi nikdy: prvni zprava jen oznami, ze hovor zacal (in-progress,
data null), a prave ta ticket zaklada. Hodnoceni prijde az posledni zpravou,
kdy uz ticket existuje. Stav byl jedina vyjimka, protoze ho krok nastavuje
zvlast pres updateTicketStatus - proto fungoval a zbytek ne.

Jedno pravidlo misto dvou seznamu poli:
- neprazdna hodnota prepise, prazdna nemaze. IntakeInput ma na to `apply`,
  v `create` zustala jen zaloha predmetu a vychozi stav
- prazdna hodnota nemaze schvalne. Prave to byla puvodni obava, kvuli ktere se
  zapisovalo jen pri zalozeni: pozdejsi zprava bez jmena zakaznika je bezna
  a smazat kvuli ni jmeno by bylo horsi nez ho nedoplnit
- vyjimky zustavaji dve: zaloha predmetu z externiho ID plati jen pri vzniku
  a stav chodi pres updateTicketStatus, ktere resi i priznak vyrizeni, cas
  vyreseni a pocet znovuotevreni

Data smi chodit po castech:
- vlastni pole typu se scitaji podle klicu. Prvni zprava posle `data`, druha
  `data2` a ticket ma obe
- prazdny retezec pole nemaze. Sablona, ktera na nic neukazuje, se dosadi
  prazdnem, takze {"vysledek":"{{result}}"} u zpravy bez vysledku posilalo
  prazdno a prepsalo tim hodnotu z minule zpravy. Vymazat pole jde poslanim
  null, coz uz je zamer

Dalsi dve veci, ktere u toho vyplavaly:
- create.status se do createTicket vubec nepredaval, takze ticket vznikl
  s vychozim "Nový" a hned se prepsal. V logu pak stalo "stav Nový ->
  completed" u ticketu, ktery v nem nikdy nebyl
- faze byla zrusena uz driv, ale v katalogu po ni zbyval krok "Posunout do
  dalsi faze" a pole Faze u zalozeni ticketu. Ticket ani typ ticketu fazi
  nemaji, takze krok by selhal na chybejicim skriptu a pole se zahazovalo.
  Oboji je pryc

Krok navic v logu rekne, co doplnil: "doplnen TK-123, stav completed, obsah".
Driv radek jen oznamil, ze se ticket doplnil, a nebylo poznat cim.

Overeno na bezici instanci s vlastnim DATA_DIR, tremi zpravami o jednom hovoru:
prvni zaklada ticket s prazdnym obsahem, druha doplni obsah i zakaznika, treti
bez dat je nechava byt a meni jen stav. Scenar s `data` a pak `data2` ma na konci
obe hodnoty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 08:09:05 +02:00

18 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. Volá se jednou při startu, nikde jinde.
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) src/runtime/queue.ts Vezme další práci, spravedlivě po firmách. Místo, kde nad Postgresem musí být SKIP LOCKED.
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.
findPersonByExternalId(...) src/data/people.ts Řešitel podle ID z cizí aplikace, například voicebotId.
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() src/data/bootstrap.ts Obnoví všechny kopie v paměti. Volá se po zápisu, který je může změnit.
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.

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, need, get, applyRules, fillJson. Skript nemá sahat na nic jiného.
createRedactor(...) src/scripts/util.ts Vyškrtá tajemství z textu před logováním. Používá se u všeho, co jde do logu.
applyRules, fillJson src/scripts/mapping.ts Transformace dat: pole na pole s převody, nebo objekt na objekt. Viz 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 <SLUZBA>_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<T> lib/api.ts Jediná cesta na API: base path, token, ApiError s celým hlášením ze serveru.
useApiQuery<T> lib/useApiQuery.ts Načtení dat do stránky včetně reload. S body pošle POST (dávkové načtení), s enabled: false se neptá vůbec.
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.