Resitel uz neni vlastni zaznam spojeny s uctem pres e-mail: je to clenstvi uctu ve firme a jeho ID je ID uctu. Popisek, kapacita, externi ID a zapnuti visi na clenstvi, takze clovek ve dvou firmach je v kazde jinak a spravce firmy ho vypne jen u sebe. Odebrani z firmy odebere jen clenstvi. Stara data se pri startu jednou prevedou (migratePeople.ts), vcetne odkazu v ticketech, skupinach, automatizacich, akcich a widgetech. Sprava lidi v zalozce Lide zaklada ucty, pozvanka uz nema volbu resitele. Zalozeni firmy z registru ARES: hledani podle IC nebo nazvu, dotazeni IC, DIC, sidla a pravni formy, vyber soucasnych statutarnich zastupcu a prokury, ucty spravce firmy s nahradnim e-mailem IC-poradi@placeholder.cz. Vychozi rozlozeni dashboardu bez resitele neobsahuje list.myTickets. Prepinani jazyku je docasne schovane (MULTILANG_ENABLED), web je cesky. Dokumentace aktualizovana. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
26 KiB
26 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. |
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)
| 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. |
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, 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) |
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
- Nová entita v nastavení:
defineStorev modulu entity, řádek vbootstrap.ts,crudRoutervsettings.ts, popis vSettings.tsx. Nic jiného se psát nemusí. - 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řeswithCache. Obojí nikdy. - Chybu se nesmí zkracovat. Server vrací celé hlášení, klient ho umí
zobrazit (
ErrorDetail). - Klient nepočítá práva. Co smí, říká
accessFor.