Files
csbot-prototype/documentation/05-dashboard-a-builder.md
T
JiriUhlir 5d41600578 Dialog jako jedna vrstva, hlavicka bez rozmazani, znacka Zive odstranena
Otevreny dialog sekal video vedle portalu. Posledni rozmazana vrstva na
dashboardu byla lepici hlavicka (backdrop-blur-xl); pod prekryvem dialogu
ji prohlizec prepocitaval pri kazdem prekresleni (blikajici kurzor,
kolecko obnovy dat). Hlavicka ma plnou barvu, dialog je jeden fixed prekryv
s vycentrovanym panelem. Tecka a znacka Zive odstraneny z dashboardu
i z ukazkoveho panelu na webu, duplicitni blok prefers-reduced-motion
v index.css sloucen. Dokumentace 05, 22, 99.
2026-09-09 20:55:13 +02:00

13 KiB

05 - Dashboard, builder automatizaci a simulace

Stranky portalu

/dashboard                  prehled: dlazdice, graf za 14 dni, posledni tickety a incidenty
/dashboard/automatizace     seznam a zalozeni nove
/dashboard/automatizace/:id builder: strom akci
/dashboard/konektory        katalog sluzeb, jejich spousteču a akci
/dashboard/tickety          seznam, filtry, prehled vytizeni tymu
/dashboard/tickety/:id      detail ticketu: prubeh a log, resitel, zakaznik
/dashboard/incidenty        prehled incidentu
/dashboard/nastaveni        udaje o uctu

V postrannim menu je pod Nastavenim tlacitko Simulace.

Zivy dashboard

Portal drzi jedno SSE spojeni pro celou aplikaci. Zajistuje ho EventStreamProvider v web/src/components/dashboard/.

  • Stav spojeni se v liste neukazuje. Ukazatel "Zive" s teckou byl odstranen (rozhodnuti zadavatele); vypadek spojeni se pozna podle toho, ze se prestanou objevovat bubliny udalosti.
  • Prichozi udalosti ukazuje EventToasts jako bubliny vpravo dole.
  • Data se obnovuji sama. useApiQuery ma volitelny refetchOn se seznamem typu udalosti, po kterych se ma dotaz zopakovat. Vice udalosti tesne po sobe se slouci do jednoho nacteni - debounce 150 ms je spolecny pro vsechny hooky, takze jedna udalost je jedna vlna requestu, ne pet v peti chvilich.

Pri vypadku se stream znovu pripojuje s exponencialne rostoucim odstupem az do 15 sekund, aby pri vypadku serveru neubijel provoz. Na 401 a 403 se pripojovat prestane: token vyprsel a klient vyvola auth:expired, po kterem se portal odhlasi a Login rekne, ze prihlaseni vyprselo.

Obnova neodmontuje stranku

useApiQuery vraci loading jen do prvnich dat, potom uz refreshing. DataState pri refreshing nechava deti vykreslene a jen ukaze, ze bezi obnova. Prvni verze prepnula loading pri kazde udalosti ze streamu a DataState vykreslil spinner misto obsahu - pri behu automatizace se stranka nekolikrat za sekundu odmontovala a namontovala, vcetne ztraty kurzoru v rozepsanem poli.

useApiQuery<T>(path, { refetchOn?, body?, enabled?, patchOn? })
  -> { data, loading, refreshing, error, total, reload }

Dotazy sdili cache modulu a deduplikaci bezicich requestu (klic je firma, cesta a telo), takze dve komponenty se stejnym dotazem se ptaji jednou. patchOn opravi data v cache primo z udalosti: lib/ticketEvents.ts bere payload.ticket z ticket.updated a vymeni radek v seznamu bez dotazu.

Ciselniky jsou v klientskem skladu

Lide, skupiny, typy ticketu, sluzby, konektory a pristup se nectou stranka po strance, ale ze skladu lib/collections.tsx: useCollection(key), useAccess(), useCollectionSelector. Kolekce se nacte pri prvnim pouziti, vymaze se pri prepnuti firmy a odhlaseni a opravuje se z udalosti entit (person.updated a podobne): zaznam z payloadu se vlozi nebo smaze, a kdyz payload zaznam nenese, nacte se ta jedna kolekce znovu.

Rozhodnuti majitele produktu je stredni cesta: ciselniky do skladu, tickety, behy a statistiky zustavaji dotazy na server se strankovanim. Jsou velke, meni se porad a strop viditelnosti pocita server - klientska kopie by je ukazovala jinak nez prehled vytizeni.

Filtry seznamu ticketu jsou v URL. Nalez jde poslat kolegovi a tlacitko zpet vrati predchozi filtr; driv byl filtr stav stranky a po obnoveni zmizel.

Simulace provozu

Modalni okno se otevre tlacitkem Simulace. Umoznuje:

  • zalozit ticket z vybraneho kanalu vcetne celeho logu, ktery k nemu vede, a s prepinacem, jestli se zakaznik v CRM dohleda nebo ne,
  • vyvolat incident s vlastnim popisem, sluzbou a zavaznosti,
  • vyresit prvni nevyrizeny ticket nebo bezici incident,
  • spustit automatizaci uspesne nebo s chybou.

Kazda akce opravdu meni data na serveru, takze se projevi i v seznamech a v souhrnu, ne jen v bublinach.

Strom akci

Automatizace se sklada z spoustece a kroku. Krok je bud akce nad konektorem, nebo podminka se dvema vetvemi - proto je to strom, ne seznam.

interface AutomationFlow {
  trigger: {
    connectorId: string;
    operationId: string;
    fields: TriggerField[];   // vstupni parametry
    webhookToken?: string;    // generuje vyhradne server
  } | null;
  steps: FlowStep[];
}

type FlowStep =
  | { id: string; kind: 'action'; connectorId: string; operationId: string }
  | { id: string; kind: 'condition'; fieldId: string; operator: string;
      value?: string; yes: FlowStep[]; no: FlowStep[] };

Podminka odkazuje na field.id, ne na nazev. Prejmenovani parametru proto existujici podminky nerozbije.

Misto vlozeni urcuje FlowPath v web/src/lib/flow.ts: prazdne pole je hlavni sekvence, [{ stepId, branch }] je vetev konkretni podminky.

Odkud se berou vstupni parametry

Jsou dva druhy spousteču a lisi se tim, kdo urcuje jejich parametry.

Parametry deklaruje uzivatel (webhook, formular). V katalogu maji customPayload: true. V builderu se pridavaji rucne: nazev, typ (text, cislo, ano-ne, datum) a povinnost.

Parametry urcuje sluzba (e-mail, WhatsApp, hlasova linka, ticket). V katalogu je nese providedFields. Builder je ukazuje jen ke cteni a server je pri ulozeni stromu vzdy dosadi z katalogu, jeste pred validaci. Podrobnosti a duvody jsou v 06-tickety.md, sekce "Parametry od sluzby".

U spoustece typu webhook vygeneruje server pri ulozeni adresu POST <verejna-adresa>/webhook/<token>.

Podminky pak porovnavaji hodnotu parametru, napriklad score >= 15. Nabidka operatoru se ridi typem, na cislo nejde pustit "obsahuje". Tabulka operatoru je na obou stranach - src/data/conditions.ts a web/src/lib/flow.ts. Server je autorita, kopie na klientovi existuje jen proto, aby UI nenabidlo nesmysl. Pri zmene upravit obe.

Dokud spoustec nema zadny parametr, nejde pridat podminka - nebylo by podle ceho se rozhodovat. Dialog to vysvetli.

Sirka karet pri zanoreni

Vetve ANO a NE jsou vedle sebe jen tehdy, kdyz je na to v dane karte misto. Rozhoduje sirka karty, ne sirka okna - pouzivaji se container queries (@container a @2xl:grid-cols-2 v FlowCanvas.tsx).

Karty jsou rozdelene po druhu kroku: flow/ActionCard.tsx, ConditionCard.tsx, ForeachCard.tsx, StepControls.tsx, spolecne typy v canvasTypes.ts. FlowCanvas.tsx uz jen sklada. Karty jsou v memo, callbacky se predavaji podle ID kroku a collectScopes je memoizovane - u stromu o padesati krocich byl driv kazdy stisk klavesy v poli prekreslenim vseho.

Odchod z rozepsaneho stromu hlida hooks/useUnsavedChanges.ts.

Stranka pages/dashboard/AutomationDetail.tsx uz jen drzi stav stromu a ukladani. Casti bez vlastniho stavu jsou ve flow/: TriggerConfig.tsx (nastaveni spoustece, vstupy z ui/form), SampleBody.tsx (ukazka tela), ModelTree.tsx (strom modelu), WebhookCalls.tsx (posledni volani webhooku); vzorove telo sklada lib/exampleBody.ts. Prevzeti nacteneho stromu do rozepsaneho stavu dela hooks/useSyncFromSource.ts uz pri vykresleni, ne v effectu, takze stara verze neproblikne.

Duvod: kazde zanoreni pulí dostupnou sirku. S beznym lg:grid-cols-2 vypadal strom na sirokem monitoru dobre v prvni urovni a ve treti uz mel karty siroke par desitek pixelu, takze se popisy lamaly po jednom slove. Container query se od urcite hloubky sama prepne na vetve pod sebou.

Ze stejneho duvodu se v uzke karte skryva popis akce (hidden @xs:block), zmensuje ikona a ovladaci tlacitka se skladaji do sloupce. Nazev kroku a nastaveni poli zustavaji vzdy videt - to je to podstatne.

Pri uprave stromu nepouzivat sm: / lg: na veci uvnitr karet. Reaguji na okno a v zanoreni lzou.

Co je v kterem kroku videt

Krok vidi parametry spoustece plus vystupy vsech kroku pred nim. Akce muze v katalogu deklarovat outputFields, napriklad "Dohledat firmu" vraci customerKnown a companyName. Podminka i sablona se na ne muzou odkazat.

Vetev podminky nepridava nic do sekvence za podminkou, protoze nemusela probehnout. Vypocet je v src/data/flowScope.ts, kopie pro UI ve flow.ts.

Odkaz na parametr, ktery ve strome vubec neni, je chyba 400. Odkaz na parametr, ktery vznika az v pozdejsim kroku (typicky po presunuti kroku), je nedodelek - ulozi se a rekne se, ze podminku staci posunout niz.

Nastaveni kroku

Akce muze mit nastavitelna pole (inputs v katalogu). Vyplnuji se primo na karte kroku ve strome. Hodnota je sablona, {{nazev}} se nahradi parametrem spoustece, takze jde rict "do obsahu ticketu dej text zpravy z WhatsApp".

Pod poli je nabidka parametru, kliknuti vlozi odkaz na pozici kurzoru.

Zatim to maji ticket, e-mail a WhatsApp. Ostatni akce maji jen fields, coz je pouha napoveda - builder u nich napise, ze je zatim nejde nastavit. Cilovy stav je prevest vsechny.

Podrobnosti vcetne toho, proc se odkazuje jmenem a ne ID, jsou v 06-tickety.md, sekce "Sablony".

Validace

Rozlisuji se dve veci:

Chyby vraci 400 a neulozi se: neexistujici konektor nebo operace, operace spatneho druhu, podminka na neexistujici parametr, operator nesedici na typ, duplicitni nebo nevalidni nazev parametru, nastaveni pole, ktere akce nema.

Nedodelky se ulozi, jen brani zapnuti: chybi spoustec, zadny krok, webhook bez adresy, podminka bez hodnoty, nevyplnene povinne pole akce, sablona odkazujici na parametr, ktery uz neexistuje. Vraci se v poli issues a builder je vypise. Rozdelana prace se nikdy nezahazuje.

Pridani konektoru

  1. Pridat zaznam do katalogu v src/data/services/catalog/<skupina>.ts vcetne triggers a actions. ID operace musi byt v ramci sluzby unikatni, checkOperationIds() duplicitu pri nacteni zaloguje - druha by tise prekryla prvni.
  2. Pokud pouziva novou ikonu, doplnit klic do web/src/lib/connectorIcons.ts. Musi existovat v lucide-react.
  3. Pokud patri do nove kategorie, doplnit ji do connectorCategories a do typu ConnectorCategory na obou stranach.
  4. Pokud spoustec predava vlastni data, deklarovat je v providedFields. ID parametru musi zustat stabilni, odkazuji se na ne podminky v ulozenych stromech.

Builder i katalog ji vezmou automaticky.

Co chybi

Chybi Poznamka
inputs u zbylych konektoru zatim ticket, kanaly, CRM a AI, ostatni maji jen fields
Drag and drop presouvani je zatim tlacitky nahoru a dolu
Strankovani v portalu server limit a offset umi, seznam ticketu si zatim bere vse

Co je videt v kterem kroku

Krok vidi parametry spoustece a vystupy kroku pred nim. Vystupy z vetvi podminky plati i za podminkou, jen jako nepovinne - probehla prece jen jedna vetev.

Diky tomu jde poskladat bezny postup, kde hodnotu dava jednou jedna vetev a jednou druha:

Najit kontakt (podle ICO)
podminka: nenalezeno?
  ano -> Najit kontakt (podle e-mailu)
         podminka: porad nenalezeno?
           ano -> Zalozit kontakt
Vlastni skript: partnerId = prvni vyplnene z ICO / mailu / zalozeni

Odkaz se jmenem kroku je jednoznacny ({{st_ico.contactId}}), takze dva kroky se stejne pojmenovanym vystupem si neprekazi. Hole jmeno ({{contactId}}) znamena ten posledni vystup, ktery probehl.

Prace nad celym modelem

Odesilatel neposila ploche telo. Objednavka ze Shoptetu ma zanoreni, ceny v podobjektech a seznam polozek - a to je bezny pripad, ne vyjimka.

Ukazka tela

U spoustece s vlastnimi daty (webhook, formular) se vlepi telo z realneho volani. Server z nej odvodi model, tedy seznam cest i s typy (src/data/model.ts), a ten se v krocich nabizi ke kliknuti.

Neni to kontrakt: telo se nezahodi, kdyz se od ukazky lisi. Je to napoveda pro cloveka - prave ta u modelu o padesati cestach chybela.

Tlacitko Doplnit parametry pro podminky z hodnot v ukazce udela deklarovane parametry. Podminka se totiz pta na parametr, ne na cestu.

Odkaz muze byt cesta

Odkaz Co vrati
{{callSid}} deklarovany parametr, jako driv
{{data.order.code}} hodnotu z prijateho tela
{{data.order.items[0].name}} prvni polozku seznamu
{{st_faktura.invoiceId}} vystup kroku st_faktura
{{item.amount}} polozku uvnitr smycky

Overuje se jen prvni cast odkazu. Zbytek je cesta a tu predem overit nejde - co presne prijde v tele, vime az pri behu. Diky tomu ploche odkazy funguji dal presne jako driv.

Pro kazdou polozku

Krok foreach projde seznam a za kazdou polozku vykona vnoreny podstrom.

Pro kazdou polozku: data.order.items
  +- CRM: upsert produktu   kod {{item.code}}, nazev {{item.name}}

Uvnitr je {{item}} a {{index}}. Po skonceni je {{st_loop.results}} seznam vysledku, kde kazdy zaznam nese index, celou puvodni polozku a vystupy kroku:

[{ "index": 0, "item": { "code": "PROD-001", "amount": 2 }, "crmItemId": "42" }]

Proc i puvodni polozka: radek objednavky potrebuje jak ID z CRM, tak mnozstvi z puvodnich dat. Kdyby se neslo obe, muselo by se to znovu parovat podle kodu.

Na tenhle seznam pak jde poslat krok Transformace s prevodem Za kazdou polozku seznamu a poskladat z nej radky objednavky jednim volanim.

Strop 200 polozek. Vic uz neni automatizace, ale zatez, kterou nikdo necekal. Beh se zastavi a rekne to, misto aby seznam tise usekl.

Kdyz na ceste neni seznam, krok selze a v hlasce stoji, co tam misto nej je.