# 99 - Zaznam zmen Nejnovejsi nahore. ## 2026-08-25 - chybova hlaseni konektoru rikaji, co se stalo "Pristup zamitnut: GET /apps/idoklad/account/agenda vratilo HTTP 403. Zkontrolujte pristupove udaje." Tahle hlaska je k nicemu. 403 muze byt nepovolena IP adresa i spatne udaje a veta radi presne to, co v tu chvili nepomuze. Duvod pritom sluzba do tela odpovedi napsala, jen se zahodil. ### Zmeneno - **Duvod od sluzby jde primo do hlasky.** `src/scripts/http.ts` vytahne z tela odpovedi `detail`, `error_description`, `message`, `title`, `error` i seznam `missingHeaders`. Retezec, ktery vypada jako JSON, se rozbaluje dal - nase sluzba iDoklad presne takhle predava telo od iDokladu samotneho. Kdyz sluzba nenapsala nic, hlaska to rekne, misto aby to zamlcela. - **401 a 403 uz nejsou jedna hlaska.** 401 = udaje sluzba dostala a neuznala, IP adresa s tim nema co delat. 403 = tvar udaju je v poradku, zakazuje se samo volani, tedy IP adresa, opravneni uctu nebo aplikace, pod kterou se vola. - **Cela odpoved sluzby je u overeni konektoru rozbalena rovnou** (`ErrorDetail` ma novy `defaultOpen`). U chyby, kterou nikdo necekal, je slozeny toggle to same jako zadny detail. ### Pridano - **Cela adresa vcetne serveru v kazde hlasce.** `ScriptRequestInfo` ma nove `url` (origin a cesta, bez query - v query muze byt tajemstvi). Do te doby hlaska rikala jen `/apps/idoklad/account/agenda`, coz nerekne, jestli se to trefilo na spravny stroj, nebo to zaridla cizi proxy cestou. Zaklad adresy je z konfigurace a konektor ho smi prepsat, takze se neda odvodit z toho, kde je nasazeny portal. Adresa je videt i na karte konektoru, v hlavicce dialogu Logy a v odpovedi na test i kdyz projde (`baseUrl`). - **Tlacitko Logy na karte konektoru a historie poslednich peti overeni.** Dosud odpoved sluzby existovala jen v odpovedi na test, tedy do prekresleni stranky, a v logu containeru. Do logu containeru se nikdo divat nechodi. Zaznam se uklada i pri uspechu, jinak by neslo poznat, jestli konektor nesel nikdy, nebo prestal jit ve chvili, kdy nekdo sahnul na udaje. Migrace `003_connector_checks.sql`, endpoint `GET /api/dashboard/connectors/:id/checks`. ### Opraveno - **Dialog se vykresluje portalem do `document.body`.** Samo `position: fixed` nestaci: rodic s `backdrop-filter` (nase `.glass`, tedy skoro kazdy panel a karta) je pro fixed potomka containing block. Dialog se pak vesel do te karty misto pres celou obrazovku. Tykalo se to vsech dialogu, jen to bylo videt az u Logu, ktere jsou v male karte konektoru. ### Pridano pozdeji - **Vybrane hlavicky odpovedi u chyby** (`ScriptError.responseHeaders`): `server`, `via`, `content-type`, `www-authenticate`, `retry-after`, `x-request-id`, `date`. Rikaji, kdo odpoved vydal - `Server: Kestrel` je aplikace, `Via: 1.1 Caddy` proxy pred ni. U 403 od proxy byva telo prazdne a bez hlavicek by nezbylo nic. Allowlist, ne vsechno: `Set-Cookie` a podobne do zaznamu nepatri. ### Nedoreseno Proc iDoklad vraci 403, zatim nevime. Vylouceno je to, co posilame: zadna kombinace hlavicek (`Idempotency-Key`, `Accept`, User-Agent) 403 nevyvola, sluzba na ne odpovida 401 jako na cokoliv jineho. Zbyva **zdrojova IP adresa naseho containeru** nebo **403 od iDokladu samotneho**, ktere sluzba jen predava dal. Zvenci to reprodukovat nejde: pres dvacet variant hlavicek (`Idempotency-Key`, `Accept`, User-Agent, jazyk, delka a tvar udaju, duplicitni hlavicky, HTTP/1.1) vraci vzdycky 401. To ale nic nedokazuje o IP adrese containeru - z povolene IP se pochopitelne projde. Rozhodnou hlavicky odpovedi a jeji telo, obojí je nove v portalu pod Logy. Sonda na `/health` vedle overeni byla spatny napad a je pryc: `/health` povoleni IP adresy nevyzaduje, takze z toho, ze projde, o IP nic neplyne. ## 2026-08-20 - vystup z vetve plati i za podminkou Pri stavbe cesty "objednavka -> faktura" vyslo najevo, ze se bezny postup neda poskladat z kroku: **najdi zakaznika podle ICO, kdyz neni, zkus e-mail, kdyz porad neni, zaloz ho** konci tim, ze ID odberatele dava jednou jedna vetev a jednou druha. Rozsah ale vystupy z vetvi za podminku nepoustel, takze se dal pouzit jen tak, ze se hledani a zakladani sloucilo do jednoho kroku. To bylo obejiti nasi vlastni chyby, ne reseni. ### Zmeneno - **Vystup z vetve je za podminkou k dispozici**, jen jako nepovinny (`required: false`). Ze hodnota muze chybet, se neztratilo - builder to u pole ukaze a pri behu se dosadi prazdno, stejne jako u ceho jineho, co neprislo. - **Vystup se stejnym jmenem uz z nabidky nemaze ten starsi.** Po druhem hledani kontaktu zmizelo ID z toho prvniho, tedy presne to, co je v tu chvili potreba. Odkaz se jmenem kroku (`{{st_ico.contactId}}`) je jednoznacny, duvod k mazani neni. Hole jmeno (`{{contactId}}`) porad znamena ten posledni. - **Duplicitni jmena u vystupu kroku uz nejsou nedodelek.** Dva kroky, ktere vraci `contactId`, jsou bezna vec. Konflikt zustava tam, kde opravdu je: mezi parametry spoustece, kde zadny prefix neni. ### Pridano - **Krok Zalozit kontakt** (`idoklad.create-contact`). Nic nedohledava - kdyz uz kontakt existuje, vznikne druhy, a to je spravne chovani teto operace. Hledani je vlastni krok, takze si strom sam rekne, kdy hledat a kdy zakladat. - `idoklad.upsert-contact` (Najit nebo zalozit) zustava pro toho, komu staci "chci mit jistotu, ze tam je". Obojí ma smysl, ani jedno nenahrazuje druhe. ## 2026-08-20 - vlastni skripty firmy: prevod dat v JS Klikaci pravidla jsou u peti poli rychlejsi, ale u modelu objednavky je jich dvacet a v tom se necte. Proto vedle nich **skript firmy**: prevod z A do B napsany v JavaScriptu, jeden na zakaznika. ### Pridano - **Skripty firmy** (`src/data/tenantScripts.ts`). Uklada se do uloziste, ne na disk - disk je uvnitr kontejneru a redeploy ho vymaze. - **Krok Transformace dat - Vlastni skript.** Vybere se skript a zdrojova data, vysledek jde dal jako `{{krok.result}}`. - **V logu je vstup i vystup.** Prave to byl duvod, proc skript nad pravidly vyhral: kdyz vysledek nesedi, neni potreba hadat, co do prevodu vlezlo. - **Zkouska bez ulozeni.** V portalu se vlepi skutecne telo a hned je videt, co z toho leze. Bez toho by se chyba poznala az z padleho behu. - Skripty jsou v zalozce **Akce**, vedle definic akci. Obojí je popis toho, co aplikace ve firme umi, a spravuje to tentyz clovek pod pravem `action.manage`. ### Co skript smi a co ne Skript je **ciste prevod hodnot**: dostane `input`, vrati objekt. Nema `require`, `import`, `process`, `fetch` ani `console`. Volani ven patri do kroku konektoru, ktery ma pristupove udaje, opakovani i zapis do logu. | Pojistka | Proc | | --- | --- | | limit 2 s | zacykleny skript by jinak zablokoval workera vsem firmam | | vysledek do 256 kB | vetsi objekt uz stejne nikdo dal nezpracuje | | kod do 20 000 znaku | delsi uz neni prevod, ale aplikace | | zadny stav mezi behy | stav, ktery prezije beh, je zdroj nejhur hledanych chyb | **Cim to neni.** `node:vm` neni bezpecnostni hranice proti nekomu, kdo se chce dostat ven. Je to izolace proti nehode a proti zacykleni. Skript pise spravce te same firmy, tedy nekdo, kdo jeji data stejne vidi. Kdyby mel skripty psat nekdo zvenku, patri to do samostatneho procesu s vlastnimi pravy. ### Opraveno pri tom - **Casovy limit nepokryval samotny beh.** Prvni verze pouzila `runInContext` jen na vyrobu funkce a zavolala ji az potom zvenku - `while (true) {}` uvnitr ni zablokovalo proces navzdy. Zjisteno pri zkousce, ktera se zasekla. Kod se ted vola uvnitr `runInContext`, takze limit plati. - Chyba z limitu nese prototyp z kontextu skriptu, takze `instanceof Error` na ni neplati. Pozna se podle textu. - `console` v novem kontextu existuje samo od sebe a psalo by do logu serveru. Odebrano. ## 2026-08-20 - prace nad celym modelem, ne nad plochym seznamem poli Odesilatel posle cely model - objednavka ze Shoptetu ma zanoreni, ceny v podobjektech a seznam polozek. Dosud se dal napojit jen plochy seznam skalarnich poli, takze `items[]` neslo pouzit vubec. ### Pridano - **Cesty v sablonach.** `{{data.order.billingAddress.city}}`, `{{data.order.items[0].name}}`, `{{st_faktura.invoiceId}}`. Hleda se nejdriv presny klic, teprve pak cesta - vystupy kroku se ukladaji i pod klic s teckou a presna shoda musi mit prednost. - **Cele telo v kontextu.** Krome pojmenovanych hodnot je k dispozici i telo v puvodnim tvaru, takze cesta funguje, aniz by to nekdo predem vypsal jako parametr. Deklarovane parametry maji prednost pri shode jmen. - **Ukazka tela u spoustece** (`trigger.sample`). Vlepi se telo z realneho volani a odvodi se z nej model, tedy seznam cest i s typy (`src/data/model.ts`). V krocich se cesty **klikaji**, misto aby se opisovaly, a kontrola vi, ze `data.order.code` neni preklep. - Tlacitko **Doplnit parametry pro podminky** z hodnot v ukazce udela deklarovane parametry. Podminka se pta na parametr, ne na cestu. - **Krok Pro kazdou polozku** (`foreach`). Projde seznam v datech a za kazdou polozku vykona vnoreny podstrom. Uvnitr je `{{item}}` a `{{index}}`, po skonceni `{{krok.results}}` se seznamem vysledku. - Kazdy vysledek nese `index`, **celou puvodni polozku** a vystupy kroku. Radek objednavky potrebuje jak ID z CRM, tak mnozstvi z puvodnich dat - kdyby se nesly obe, muselo by se to znovu parovat. - Strop je 200 polozek. Vic uz neni automatizace, ale zatez, kterou nikdo necekal, takze se beh zastavi a rekne to. - Kdyz na ceste neni seznam, krok selze s tim, co tam misto nej je. - **Prevod Za kazdou polozku seznamu** v klikacim editoru mapovani. Engine ho umel (`op: 'map'`), ale sel napsat jen rucnim JSONem. ### Overeno Nad skutecnym modelem objednavky ze Shoptetu: 23 cest vcetne `data.order.items[].unitPrice.withoutVat`, sablony s cestou i s indexem, struktura predana jako JSON, neexistujici cesta jako prazdno s hlaskou v logu, smycka nad dvema polozkami s posbiranymi vysledky. ### Co to nemeni Ploche odkazy `{{callSid}}` funguji dal presne jako driv - overuje se **prvni cast** odkazu, takze u nich se nic nezmenilo. Existujici automatizace se nemusi nijak upravovat. ## 2026-08-17 - log rekne, co zpusobilo jakou zmenu Nalezeno na bezicim serveru: ticket TK-4946 mel 177 prichozich udalosti a 620 radku v logu, pritom se nestalo skoro nic. Zmereno proti behum ve fronte: ve stejnem okne vzniklo **presne tolik behu, kolik prislo udalosti** (22 a 22), kazdy s jednim pokusem. Fronta ani worker nic nenasobi - odesilatel poslal 177 POSTu. Nasi vinou bylo, ze to z historie neslo poznat. ### Opraveno - **Data udalosti se konecne ukladaji.** `payload` byl u kazde udalosti prazdny, protoze krok `ticket/upsert` ukladal jen to, co si clovek vyplni v poli Data. Kdyz je prazdne, ulozi se **to, cim beh zacal** - u webhooku cele prijate telo. Prazdna udalost je horsi nez zadna: tvari se, ze se neco stalo, a nerekne co. - **Shodna udalost se pocita, nezaklada dalsi radek.** Kdyz prijde presne totez co posledne, pricte se k pocitadlu (`repeats`, `lastAt`) a v portalu se u radku ukaze "22x beze zmeny, naposledy ...". Ticket se pritom **nemeni**: neprepise se `updatedAt` ani se nerozesle zmena, takze duplikat nerozbliká dashboard a nespusti automatizaci navazanou na zmenu ticketu. - Zahodit duplikat nejde. Bez pocitadla by nikdo nezjistil, ze proti nam neco tluce ve smycce. - Porovnava se **jen s posledni** udalosti. "Objednavka pripravena" muze legitimne prijit znovu za hodinu - to je novy fakt. - **Zmeny se radi pod udalost, ktera je zpusobila.** Log uz mel strom (`parentId`), ale nikdo ho nepouzival. Zapisy behu se ted radi pod jeho radek udalosti, takze log se cte jako "prislo tohle -> zmenilo to tohle". - **U udalosti stoji, kdo ji prinesl**: `Prijata udalost: stav completed (automatizace TEST)`. Prvni otazka nad zmenenym ticketem je "kdo mi do toho sahl" a `webhook` na ni neodpovida. - **Poznamka o stavu jen kdyz se stav zmenil.** Predtim se u kazde udalosti zapsalo `Stav zmenen z in-progress na in-progress`, tedy tri radky logu na jednu zpravu, ktera nic nezmenila. Odtud 620 radku. - **Popisek udalosti uz neni porad "Udalost".** Bere se popisek, predmet, stav, externi ID - v tomhle poradi. Casova osa, kde je na kazdem radku totez, nerika nic. - **Opakovani nezapisuje radek do logu vubec.** Krok muze rict `quiet`, cimz se jeho radek do logu ticketu nepise - pocet opakovani je videt u prichozi udalosti, coz je jedno cislo misto osmdesati radku. - **Ticket se rodi s poslanym stavem.** Predtim vznikl s vychozim `Nový` a hned se prepsal, takze v logu stalo `stav Nový -> completed` u ticketu, ktery v tom stavu nikdy nebyl. Odtud i to `Nový`, co bylo videt ve widgetu. - Typograficke uvozovky z popisku v logu pryc, plus ctyri AI znaky, ktere zbyvaly v kodu (`web/src/data/products.ts`, `References.tsx`, `automationStore.ts`). ### `runsToday` konecne znamena dnes Bylo to pocitadlo od zalozeni automatizace, jen se jmenovalo "dnes" - na serveru ukazovalo 373 za automatizaci, ktera bezi tri mesice. - Automatizace si drzi **behy po dnech** (`days`, poslednich 14 dni). Po pulnoci je "dnes" nula, dokud opravdu neco nebezi. - Vedle toho `runsYesterday` a `runsTotal`. Celkovy pocet se pocita od zavedeni historie po dnech, protoze puvodni citac se den ode dne nedelil a rozpocitat ho zpetne neni z ceho. - Uspesnost se pocita z dnesnich behu. Kdyz dnes zadny nebyl, bere se posledni den, kdy byly - nula procent u automatizace, ktera dnes nemela co delat, by vypadala jako porucha. - V seznamu je pod dnesnim cislem vcerejsek, na detailu dnes, vcera i celkem. ## 2026-08-17 - prevzeti ticketu ze skupiny a pozvanky do firmy ### Pridano - **Prevzeti ticketu.** `POST /tickets/:id/claim`. Kdo je ve skupine, ktera ma ticket u sebe, si ho vezme sam. Prace se nerozdava shora, lidi si ji beru podle toho, kdo ma cas. - Vzit ticket, ktery uz nekdo resi, je neco jineho: to je prehozeni a chce to pravo `ticket.assign.others`. - Ticket bez resitele si vezme kdokoli, kdo je vedeny jako resitel. - **Krok `ticket/assign-group`** (Predat skupine) s prepinacem **Priradit rovnou nejvolnejsimu**. Prazdne nebo Ne = ticket zustane ve fronte skupiny. Prepinac je na kroku automatizace, ne na skupine: tataz skupina potrebuje u havarie okamzite prideleni a u bezneho dotazu ne. - **Pozvanky do firmy.** Spravce vytvori odkaz s nahodnym kodem (`/pozvanka/:kod`), posle ho, jak chce. Kdo ho otevre, vyplni jmeno a heslo a je uvnitr. - **Heslo se nikdy neposila.** Nastavuje si ho sam clovek az za odkazem. - Kdyz uz ucet ma, zada k nemu svoje heslo a jen se pripoji k dalsi firme. Bez overeni hesla by kdokoliv s odkazem pripojil cizi adresu ke sve firme a videl by jeji data. - Pozvanka plati tyden, da se zrusit a po pouziti prestane platit sama. - Volitelne z cloveka rovnou udela resitele. Ucetni muze mit pristup do portalu, aniz by kdy resila ticket, proto se to pta. ### Zmeneno - **Resitele, skupiny a pozvanky jsou na strance Lide**, ne v nastaveni. Pozvat kolegu je bezna denni prace, ne nastaveni portalu - dokud to bylo schovane v nastaveni, nikdo to nenasel. - **Cleny skupiny se vybiraji klikanim** ze seznamu lidi. Predtim se opisovala ID `ppl_xxx` oddelena carkou, coz je preklep cekajici na sve misto. ### Nove soubory | Soubor | Co dela | | --- | --- | | `src/data/invites.ts` | Entita pozvanky, kod, platnost. | | `src/routes/invites.ts` | Verejne cesty (prohlednuti a prijeti) a sprava. | | `web/src/pages/Invite.tsx` | Stranka za odkazem: jmeno, e-mail, heslo. | | `web/src/components/dashboard/InvitePanel.tsx` | Sprava pozvanek v zalozce Lide. | ## 2026-08-13 - vyrizeno je vyslovny priznak, ne hadani ze stavu ### Zmeneno - **`Ticket.closed` se nastavuje vyslovne.** Predchozi verze ho odvozovala ze jmena stavu (`completed`, `vyreseno`, ...), coz nikdo nechtel a hlavne to uhodne spatne pokazde, kdyz si nekdo pojmenuje stavy po svem. - `TicketType.closedStatuses` zruseno. Byla to tatáz obchazka o uroven vys. - Kroky `ticket/upsert` a `ticket/set-status` maji vstup **Vyrizeny** (ano/ne/prazdne). Prazdne znamena **nemenit** - jinak by kazda zmena textu stavu mimochodem otevrela vyrizeny ticket. - `POST /tickets/:id/status` prijima `closed` jako nepovinny bool. - Podminka se muze zeptat na `closed`, takze jde napsat "kdyz je vyrizeny". ### Overeno 8 kontrol: ticket se stavem `completed` **neni** automaticky vyrizeny, dokud to nekdo nerekne. Automatizace s podminkou `status = completed` zabere na ticketu, ktery do toho stavu prejde, prida stitek a nastavi vyrizeno. Na uz existujici tickety nesahne, dokud se s nimi neco nestane - spousti ji udalost, ne stav. ## 2026-08-13 - stav ticketu je volny retezec, ciselnik pryc ### Zmeneno - **`Ticket.status` je volny retezec.** Ciselnik `new | open | waiting | resolved` je pryc. Tickety chodi z cizich aplikaci, ktere maji svoje stavy - voicebot posila `ringing` a `completed`, e-shop `pripraveno k expedici`. Nutit je do nasi ctverice znamenalo, ze u ticketu svitilo "Novy", i kdyz byl podle odesilatele davno hotovy. - **Pribyl priznak `Ticket.closed`.** Fronta, vytizeni i statistiky potrebuji vedet, co uz nikdo neresi, a z volneho retezce to poznat nejde. Nastavuje se sam: podle `TicketType.closedStatuses`, a kdyz je typ nema, podle bezneho pojmenovani (`vyreseno`, `hotovo`, `completed`, `closed`). - **`stage` zruseno.** Byla to obchazka, jak dostat cizi stavy do ticketu, aniz by se sahlo na ciselnik. Kdyz je stav volny, druhe pole na tutéz vec je jen zmateni. Hodnoty z faze patri do stavu. - Vyber stavu v detailu ticketu nabizi stavy typu, doporucene a **ten, ktery ticket ma prave ted** - jinak by hodnota z cizi aplikace ze seznamu zmizela a prvni rucni zmena by ji prepsala. - Filtr v seznamu ticketu nabizi **stavy, ktere v datech opravdu jsou**, ne pevnou ctverici. - Barva u stavu: zname nazvy maji svou, cokoliv jineho neutralni. Hadat, jestli je `ringing` dobre nebo spatne, by bylo horsi nez nehadat. ### Overeno 11 kontrol proti bezicimu serveru: ticket z voicebota ma stav `ringing`, po dalsich zpravach `in-progress` a `completed`, `completed` se pozna jako hotovo, hotovy ticket zmizi z fronty, filtr nabizi stavy z dat a funguje na ne, widget je ukazuje misto ciselniku a rucne jde nastavit i `ceka na zpetne volani`. ## 2026-08-13 - nastaveni prezije nasazeni, ukazkova data uz se nevraci Bez databaze lezi data uvnitr containeru, takze **redeploy je smaze** a seed je nasype znovu. Dokud nebude Postgres, resi se to takhle: ### Zmeneno - **Ukazkova data jen se `SEED_DEMO=1`.** Automatizace, tickety a incidenty se uz po kazdem nasazeni nevraci. Konfigurace (firmy, uzivatele, role, resitele, typy, widgety) se nasypava dal - bez ni je portal nepouzitelny. - **Token webhooku z `WEBHOOK_TOKEN_TEST`.** Driv se pri kazdem nasazeni vygeneroval novy, takze odesilatel musel prepisovat adresu. Ted je token v promenne aplikace: neni v gitu a adresa se nemeni. - **Automatizace, ktera na instanci opravdu bezi, je v seedu.** Je to provizorium, ne cil - az data prezijou nasazeni, patri zpatky do dat. ### Pridano - `ticket/upsert` umi **vsechna pole ticketu**: zakaznik (firma, kontakt, kam odpovidat), kanal, odkaz na zdroj, priorita, stitky, resitel, skupina a vlastni pole typu jako JSON. Zakaznik a kanal se vyplnuji jen pri zalozeni, aby pozdejsi udalost s prazdnym jmenem neprepsala, co uz tam je. - **Kroky ve strome jdou sbalit**, vychozi je sbaleno. Sbaleny krok ukazuje, co ma vyplneno, ne popis operace, ktery uzivatel zna. - Seskupovani widgetu **podle faze** a dva nove widgety: tickety podle stavu a podle faze za tento mesic. Obojí se proklikne na vyfiltrovany seznam. - Filtr na fazi v seznamu ticketu vcetne adresy. ### Opraveno - **Faze se ukazuje jako stav.** Kdyz ticket ma fazi z workflow sveho typu (napr. stav hovoru od voicebota), ukazuje se ona; nas zivotni cyklus zustava vedle jako drobny text, protoze se z nej pocitaji statistiky. Driv byl videt jen nas ctyrprvkovy ciselnik, coz u ticketu z cizi aplikace nedava smysl. ### Overeno 18 kontrol proti bezicimu serveru: po startu bez `SEED_DEMO` je tam jedna automatizace a nula ukazkovych ticketu i incidentu, token webhooku sedi s promennou, provoz na te same adrese zaklada tickety, krok vyplni vsechna pole vcetne vlastnich a oba widgety pocitaji a prokliknou se. ## 2026-08-13 - krok prirazeni a ukazkova data nesahaji na provoz ### Opraveno - **`ticket/assign` nemel vykonnou cast**, takze krok "Prirad resiteli" vzdycky selhal hlaskou "operace nema vykonnou cast". Doplnen jako vnitrni krok. - **Ukazkova automatizace "Smerovani ticketu na resitele" je vypnuta.** Zapnuta prebirala tickety, ktere uz nekomu patrily podle skutecne automatizace zakaznika - ukazkova data nemaji sahat na zivy provoz. - Do udaju spoustece pribylo `assigned` a `knownCustomer`, aby slo napsat podminku "uz je prirazeny, nesahej na to". Bez toho nemela z ceho vychazet. ### Zmeneno - `ticket/upsert` prijima `status`. Kdyz hodnota patri mezi nase ctyri stavy, nastavi stav; jinak se ulozi jako **faze**, protoze to presne je - cizi aplikace posila svoje stavy hovoru a nas zivotni cyklus je pevny. Do shrnuti kroku se napise, co se stalo, aby to nebylo kouzlo. ### Overeno Pripad z provozu: telo `{callSid, status, voicebotId}`, callSid do externiho ID, voicebotId jako stitek, status jako stav. Ctyri zpravy o trech hovorech daly **tri tickety**, filtr na stitek vratil jen hovory daneho voicebota a widget je spocital (vb-77: 2, vb-88: 1) vcetne prokliku na vyfiltrovany seznam. ## 2026-08-13 - fronta, worker a spoustece Popis v [20-fronta-a-runtime.md](20-fronta-a-runtime.md). ### Zmeneno zasadne - **Webhook uz nic nevykonava v requestu.** Zapise udalost do fronty a odpovi 202 do jednotek milisekund. Strom vykona worker na pozadi. Za konektory nerucime, takze cekat na cizi sluzbu v requestu znamena ztracet udalosti. ### Pridano - `runtime/queue.ts`: fronta behu v ulozisti. Opakovani s rostouci prodlevou (30 s, 2 min, 10 min, hodina), spravedlive poradi po firmach, navrat zaseknutych behu po restartu, uklid hotovych. - `runtime/worker.ts`: bere praci z fronty, ctyri behy naraz. - `runtime/triggers.ts`: tri druhy spoustecu. Push (webhook), vnitrni udalost (vznik a zmena ticketu) a **pull, tedy pravidelne dotazovani** u sluzeb, ktere webhooky nemaji - posta, zpravy. Planovac jen rekne "je cas", samotny dotaz je prvni krok stromu. - **Kontrakt tela webhooku.** Kazdy parametr ma cestu (`data.order.id`, `errors.0.message`), takze jde napojit i odesilatel s vnorenym modelem. U adresy je videt metoda, ukazka tela podle parametru a kopiruje se cela adresa vcetne domeny. - Vnitrni kroky: `ticket/upsert` (zaloz nebo doplň podle externiho ID), `assign-least-busy`, `assign-by-external`, `set-type`, `set-stage`, `add-tags`, `set-status`, `incident/create`, `flow/pause`, `flow/log`. - **Faze ticketu** (`stage`) jako treti osa vedle stavu a stitku. Stav je zivotni cyklus a pocitaji se z nej statistiky, faze je workflow daneho typu a muze byt jen jedna, takze se na ni da spolehnout v podmince. - **ID z cizich aplikaci u resitele** (`externalIds`). Voicebot posle `voicebotId` a ticket skonci u toho, komu patri. Vazba je na jednom miste, ne v kazde automatizaci. - **Upozorneni**: komu prijde ticket, ten to vidi hned, vcetne cisla u zalozky. - **Incident z kazde chyby** se dvema urovnemi: `impact` cte klient a je srozumitelny, `detail` cte admin a je v nem cely beh, ktery krok selhal, cele hlaseni a data na vstupu. `detail` se vraci jen spravci platformy. - **Ochrana proti smycce.** Automatizace navazana na zmenu ticketu ticket meni, cimz se spousti znovu - pri vyvoji to server polozilo. Resi to oznaceni behu (`AsyncLocalStorage`) a strop peti behu na ticket za minutu. - Zivy dashboard: dlazdice nad nasimi daty se prekresli na udalost, data z konektoru drzi server podle `ttlSec` a jde vynutit nacteni znovu. ### Opraveno - `path` a `intervalSec` u spoustece se pri ulozeni zahazovaly, takze kontrakt webhooku nefungoval. - Nad seznamem neslo pouzit `contains`, takze na stitky neslo postavit podminku. Prave na tom stoji prideleni prace. - Novejsi vystup kroku ted prekryje starsi se stejnym jmenem. Driv to builder hlasil jako konflikt i tam, kde zadny nebyl. - Marna chyba (chybejici skript, neexistujici skupina) se uz neopakuje petkrat. ### Overeno Dva scenare proti bezicimu serveru, 34 kontrol celkem: 1. **Firma se skladem, expedici a IT.** Webhook odpovedel za 12 ms, worker zalozil ticket, dal mu typ a stitek, druha automatizace ho podle typu a stitku predala nejvolnejsimu ze skladu. Druha objednavka sla jinemu cloveku. Chyba z prevodniku dokladu prisla vnorenou cestou, skoncila u IT a zalozila incident. 2. **Hovory z voicebota.** Telo `{callSid, status, voicebotId}`: callSid do externiho ID, status do faze, prirazeni podle voicebotId. Tri zpravy o tomtez hovoru daly **jeden ticket** se tremi udalostmi. Neznamy voicebot neskoncil tise - je videt ve fronte i jako incident. ## 2026-08-13 - runtime, prokliky z widgetu a kapacitni rozbor ### Pridano - `src/runtime/executor.ts`: **strom se konecne vykonava**. Jde krok po kroku, u podminky se vetvi, do poli dosadi `{{parametry}}`, akci pusti pres `runScript` a vystupy pripise do kontextu, aby na ne mohl dalsi krok odkazat. Cely prubeh jde do logu ticketu vcetne toho, co sluzba vratila. Pouzivaji ho **obe** cesty: akce na ticketu i webhook automatizace. - Z widgetu se da prokliknout na to, co je za cislem: z lidi na cloveka, ze seskupeni na uz vyfiltrovany seznam ticketu. Odkazy sklada **server**, protoze on jediny zna filtr widgetu. - Seznam ticketu cte filtr z adresy (`?typeId=`, `?tag=`, `?assignee=`, ...), takze proklik vede na spravny vyber a odkaz jde poslat kolegovi. - Seznam ticketu umi filtrovat na typ, tag a skupinu. - Spolecna `TicketTable`: jedna tabulka pro seznam i pro detail osoby. **Na mobilu se neposouva do strany**, uzka obrazovka dostane karty. - [19-kapacita-200-firem.md](19-kapacita-200-firem.md): zmereny rozbor toho, co se stane pri 200 firmach, a co to bude chtit za server. ### Opraveno - **Rozlozeni dashboardu s vlastnim widgetem se nedalo ulozit.** `validateLayout` znala jen vestaveny katalog, takze kazdy pokus skoncil hlaskou, ze widget v katalogu neexistuje. Katalog je ted jedna funkce (`widgetCatalog`) a pouziva ji nabidka i kontrola. - Katalog widgetu je za konkretni firmu, ne za vsechny firmy uzivatele. Driv slo polozit dlazdici jedne firmy na dashboard druhe, kde k ni data nikdy neprisla. ### Odebrano - **Simulace** vcetne tlacitka, dialogu i endpointu `/api/simulate`. Provoz se ted dela prijmem udalosti, ktery je skutecny. - Trojice pohledu nad tickety. Vyber firmy je select, "moje" je prepinac - driv to delalo totez dvakrat. ### Overeno 7 kontrol proti bezicimu serveru: ulozeni rozlozeni s vlastnim widgetem, oddeleni katalogu po firmach, vykonani stromu akce i webhooku, zapis behu do logu, odkazy z widgetu a filtrovani podle nich. K tomu mereni na 5 000 ticketech, ze ktereho vychazi rozbor kapacity. ## 2026-08-13 - ticketovaci system: udalosti, externi ID, statistiky, widgety Popis v [18-ticketovaci-system.md](18-ticketovaci-system.md). ### Pridano - prijem udalosti - `POST /webhook/ticket/:token`: **jakakoliv udalost se muze stat ticketem**. Token patri firme, ne automatizaci, takze zalozit ticket jde i bez stromu. - `externalId` na ticketu, **unikatni v ramci firmy**. Dalsi udalost se stejnym ID se navesi na existujici ticket misto zalozeni druheho. Cislo a retezec jsou tentyz klic, jinak by `3` a `"3"` byly dva tickety. - Udalosti se u ticketu drzi cele vcetne prijatych dat a jdou rozbalit v detailu. Je to neco jineho nez log: log je nase stopa, udalost fakt zvenku. ### Pridano - co se meri - `firstResponseAt`, `resolvedAt`, `resolvedById` a `reopenCount` na ticketu. Bez nich neslo rict, kdo kolik odbavil ani jak dlouho zakaznik cekal. - `getAgentStats`: odbavene za obdobi, fronta, medianove casy do vyreseni a do prvni reakce, vracene tickety. Median zamerne, ne prumer. - Widget **Vykon resitelu** a detail osoby pouzivaji tutéž funkci, takze cisla sedi na obou mistech. ### Pridano - widgety - Klient konecne vola `/api/dashboard/widget-data`. Predtim endpoint existoval, ale nikdo ho nepouzival, takze vlastni widget hlasil "nepodarilo se zobrazit". - Novy zdroj `connector`: co umi zjistit napojena sluzba, jde vytahnout do dlazdice. Vola se tentyz skript jako v kroku automatizace, vysledek se cachuje (`ttlSec`, nejmene 30 s). - Mrtvy odkaz v rozlozeni jde v rezimu uprav odstranit, ne jen precist. ### Zmeneno - **Akce a widgety maji vlastni zalozku**, uz nejsou v nastaveni. Je to definice toho, co aplikace umi, stejna uroven jako automatizace. - **Telo akce se sklada stromem**, ne JSONem v textarei. Je to tentyz editor jako u automatizaci, jen misto karty spoustece je "spousti clovek". - Nova zalozka **Lide** se seznamem resitelu a detailem osoby. - Seznam ticketu i lidi ma **dva pohledy**, tabulku a dlazdice. - Dlouhe pomlcky pryc z celeho projektu (48 znaku ve 23 souborech). ### Opraveno - `createTicket` bral `typeId`, `fields`, `tags` i `assigneeGroupId`, ale **nikdy je neukladal**. Ticket zalozeny s typem tak zustaval bez typu a bez vlastnich poli. ### Overeno 21 kontrol proti bezicimu serveru v rezimu souboru: navazani druhe udalosti na tentyz ticket, oddeleni firem pri stejnem externim ID, ulozeni typu a poli z prijmu, vyreseni s casem i clovekem, zapocteni navratu z vyreseno, cisla ve widgetu vykonu, cele chybove hlaseni u widgetu nad nenapojenym konektorem, detail osoby, rozsah parametru akce, ulozeni stromu akce a navigace. ## 2026-08-13 - firmy, prava, typy ticketu, akce, widgety a uloziste pro vsechno Dodelany cely [navrh rozsireni](09-navrh-rozsireni.md) a vsechna data se ukladaji. Rejstrik novych funkci a komponent je v [15-rejstrik-funkci.md](15-rejstrik-funkci.md). ### Pridano - obecne vrstvy - `src/data/store/`: jedno rozhrani `EntityStore` a dve implementace, soubor nebo pamet (`local.ts`) a Postgres nad tabulkou `records` (`postgres.ts`). Volajici nepozna, ktera bezi. Nova entita znamena jeden `defineStore` a jeden radek v `bootstrap.ts`. - `store/cached.ts` (`withCache`) pro konfiguracni entity ctene pri kazdem requestu a `store/mirror.ts` (`withMirror`) pro provozni data, ktera se meni v pameti a po zmene se cela zapisuji. Dva pomocniky, ne osm skoro stejnych souboru. - `put` v rozhrani uloziste: zapis celeho zaznamu bez skladani patche. - `src/routes/crud.ts`: fabrika CRUD rout. Seznam, detail, zapis, mazani, pravo a audit na jednom miste. - `components/dashboard/EntityAdmin.tsx`: obecna sprava zaznamu na klientovi. Nova zalozka nastaveni je popis sloupcu a poli, ne nova stranka. ### Pridano - funkce - Firmy, uzivatele, clenstvi, osoby a skupiny resitelu se spravuji z portalu. - Role a prava jsou **data**, ne pevny seznam v kodu: katalog 26 prav, vlastni role za firmu, systemove role nejde menit. Navigace chodi ze serveru jako prunik toho, co firma ma, a toho, na co ma clovek pravo. - Typy ticketu s vlastnimi poli, tagy a vlastni workflow stavu. - Vydefinovane akce na ticketu: vazba na **typ nebo tag**, telo je jedna operace, vlastni strom, nebo skript. Server posila jen akce, ktere v dane situaci projdou podminkami a pravy - klient si nepocita, co ukazat. - Vlastni widgety dashboardu vcetne dat na jeden request a seskupeni. - Audit a prepnuti spravce na jiny ucet. Prepnuti je **vychozi jen pro cteni**, zapis se musi zapnout vedome a je videt v auditu. - Detail ticketu: CTA akci, typ, tagy, vlastni pole a prehozeni na skupinu. ### Pridano - uloziste pro provozni data Tickety vcetne logu, automatizace, incidenty a rozlozeni dashboardu se po kazde zmene zapisuji. Citace ID se pri startu dopocitaji z ulozenych zaznamu, takze novy ticket nikdy neprepise stary. ### Overeno V rezimu `file`: zmena typu, tagu, vlastnich poli, stavu, komentare, nova automatizace, novy incident a upravene rozlozeni dashboardu prezily **tvrde ukonceni procesu** a po restartu byly zpatky vcetne logu ticketu. Novy ticket dostal dalsi cislo, ne cislo existujiciho. Agent bez prav dostane 403 na spravu roli a uzsi navigaci. `passwordHash` se nikdy nevraci. Databaze se v tomhle kole neoverovala, nebylo na cem - kod pro ni je stejny a psany soucasne, ale nebezel. ## 2026-08-12 - soubor jako uloziste bez databaze Mockup se k databazi nedostane, takze pribyl treti rezim: JSON soubor. Prezije restart procesu i containeru, ale ne redeploy. ### Pridano - `src/data/snapshot.ts`: atomicky zapis (`.tmp` a prejmenovani), slucovani zapisu a dokonceni rozepsaneho zapisu pri `SIGTERM`. Rozbity soubor se prejmenuje na `.broken`, zaloguje a jede se s prazdnymi daty - aplikace, ktera nenastartuje, je pro AppFactory nefunkcni sluzba. - `src/data/connectors/local.ts`: jeden kod pro pamet i soubor, lisi se jen tim, kam se zapisuje. Nahrazuje `memory.ts` - dve implementace by se casem rozesly. - Klic k sifrovani se mimo databazi vygeneruje do `DATA_DIR/secrets.key`, takze sifrovani funguje bez nastaveni. U databaze se negeneruje: kdo ma zalohu tabulky, ma i klic ze stejneho stroje. - `DATA_DIR` v konfiguraci, `data/` v `.gitignore` a `.dockerignore`. - Hlaska v portalu rozlisuje tri nasledky: pamet (ztrata pri restartu), soubor (ztrata pri redeployi) a databaze (bez ztraty, hlaska se nezobrazuje). ### Overeno Bez databaze: konektor s vyplnenymi udaji prezil restart, v JSONu jsou hodnoty sifrovane a plaintext v nem neni. S databazi: rezim `postgres` funguje dal a klic vedle dat se nevygeneroval. ## 2026-08-12 - databaze pro konektory Konektory se ukladaji do Postgresu, pristupove udaje sifrovane. Popis v [14-databaze.md](14-databaze.md). ### Pridano - `pg` jako zavislost, pool v `src/db/pool.ts` vcetne transakci a `dbFor(tenantId)` jako sev pro budouci oddelenou databazi jednoho klienta. - Migrace ze souboru `src/db/migrations/*.sql`, pousti se pri startu pod `pg_advisory_lock` - pri rolling deployi je jinak pusti vsechny instance naraz. Jeden soubor je jedna transakce, takze pri chybe nevznikne rozdelane schema. - Sifrovani pristupovych udaju (`src/db/secretBox.ts`), AES-256-GCM s nahodnym IV a verzi klice. Nerozsifrovatelna hodnota nepada, chova se jako nevyplnena a zaloguje se - jeden rozbity konektor nesmi shodit seznam ostatnich. - Dve implementace uloziste konektoru za jednim rozhranim (`memory`, `postgres`). Rozhodnuti je jen na jednom miste, v `src/data/connectorStore.ts`. - `/health/ready` s pingem do databaze. `/health` na databazi zamerne nezavisi: kratky vypadek DB by jinak vedl k restartovani containeru. - `GET /api/dashboard/storage` a hlaska na strance Konektory o tom, ze data jsou jen v pameti. Bez toho se clovek divi, kam se podely jeho konektory. - Jediny vychozi konektor na firmu a sluzbu hlida castecny unikatni index, ne jen kod. Dva soubezne zapisy by jinak udelaly dva vychozi. ### Zmeneno - Cteni i zapis konektoru je asynchronni, vcetne validace stromu. - Databaze je volitelna. Bez `DATABASE_URL` nebo `SECRETS_KEY` se jede v pameti a rekne se to v logu i v portalu. Container, ktery nenastartuje, je pro AppFactory nefunkcni sluzba. - Kdyz jsou migrace nastavene a selzou, jede se dal v pameti. Psat do rozbiteho schematu je horsi nez neukladat. ### Overeno Proti Postgresu 16 v kontejneru: migrace, sifrovani v tabulce, preziti restartu, rozsifrovani spravnym klicem, degradace pri spatnem klici, `PATCH` bez tajneho pole, prepnuti a smazani vychoziho konektoru, a pametovy rezim bez `DATABASE_URL`. ## 2026-08-12 - transformace dat a oprava konektoru Transformace dat popsana v [13-transformace-dat.md](13-transformace-dat.md). ### Pridano - Kroky si predavaji i cele struktury. `FieldType` ma `object` a `list`. Do sablony se nedosazuji, predavaji se jako celek dalsimu kroku - proto je u nich v builderu vyber a ne textove pole. Z podminek nad nimi ma smysl jen "prisla / neprisla". - Strop na velikost struktury (`SCRIPT_MAX_VALUE_BYTES`, vychozi 256 kB). Radek s vystupem kroku je nejrychleji rostouci tabulka v systemu. - `src/scripts/mapping.ts`: cesty, prevody, pravidla a sablona JSON. Skripty ho dostanou na `ctx.util` jako `get`, `applyRules` a `fillJson`. - Dva rezimy transformace: `transform.map-fields` (pole na pole s prevody) a `transform.to-json` (sablona cileveho objektu s `${cesta}`). V sablone je marker `${...}` zamerne jiny nez `{{...}}`: sablony kroku se dosazuji driv, nez krok bezi, a stihly by ji prepsat. - Prevod `map` pro seznamy. Bez nej by slo prevest hlavicku dokladu, ale ne polozky objednavky, a doklad by byl na nulu. - `idoklad.create-invoice-from-object`: druha polovina prikladu, bere hotove telo dokladu z transformace. - Spoustec e-shopu predava celou objednavku jako objekt a polozky jako seznam. - Klikaci editor pravidel v builderu vcetne rezimu JSON pro vnorena pravidla. - Kontrola JSONu a tvaru pravidel uz pri ulozeni stromu. Preklep je nedodelek, ne chyba ukladani - rozdelana prace se nezahazuje. ### Opraveno - **Konektor neukladal pristupove udaje.** Server byl v poradku, chyba byla v prohlizeci: u pole `type="password"` prohlizec ignoruje `autocomplete="off"` a dosazuje ulozene prihlaseni. Uzivatel pak videl jednu hodnotu, React drzel jinou, a ulozilo se to, co drzel React, tedy nic. Resi to `autocomplete` `new-password`, jmena poli, ktera nepripominaji heslo, a prepinac zobrazeni, aby slo overit, co je opravdu zapsane. - Zakladani a uprava konektoru se presunuly do dialogu. Na strance jsou jen male karty - formulare rozlozene po strance byly u vic konektoru neprehledne. ## 2026-08-12 - sluzby a konektory Rozdeleni na sluzbu a konektor. Popis v [12-sluzby-a-konektory.md](12-sluzby-a-konektory.md). Slovo "konektor" driv v kodu znamenalo katalog toho, co umime. Ted znamena napojeni jedne firmy, tedy to, co tim mysli i uzivatel. ### Pridano - `src/data/services.ts`: sluzba nese `general`, `appId`, `visibility`, `credentials` a `verifyPath`. Kategorie `obecne` sdruzuje veci, ktere ma kazdy a nepotrebuji konektor: webhook, planovac, tickety, transformace dat, HTTP pozadavek, pauza, log. - Viditelnost sluzby: vsichni, jen uvedene firmy a lide, nebo jen spravce platformy. Neviditelna sluzba se z API nevraci vubec. - `src/data/connectorStore.ts`: konektory za firmu vcetne hodnot pristupovych udaju. Hodnoty se z API nikdy nevraci, jen `filled` a `missing`. - `FlowStep.connectorId`: krok rika, pod kterym napojenim se ma volat. `null` = vychozi konektor firmy, takze vzorovy strom je prenositelny. - Overeni konektoru pres `verifyPath`, tedy cteci volani vyzadujici autorizaci. U sluzby bez nej se overi jen dostupnost a odpoved to rekne nahlas. - Stranky `/dashboard/sluzby` a `/dashboard/konektory` vcetne formularu udaju. - Endpointy `/api/dashboard/services` a CRUD `/api/dashboard/connectors` vcetne Swaggeru. - Predvyplnene prihlaseni spravcem platformy a prepinac demo uctu na login strance. Kvuli testovani prototypu, pred ostrym pouzitim odebrat. ### Zmeneno - **Pristupove udaje se prestaly cist z environment variables.** Cela instance by mela jedny udaje spolecne a dve firmy by fakturovaly z jednoho uctu. Z prostredi zustava jen `SERVICES_BASE_URL`. - Stav "napojeno" se prestal cist z katalogu a zacal pocitat z konektoru firmy. Sluzba ma jen `available` nebo `planned`. - Prejmenovani napric kodem: `Connector` na `Service`, `FlowStep.connectorId` na `serviceId`, `GET /connectors` na `GET /services`, `connectorIcons` na `serviceIcons`, stranka Konektory (katalog) na Sluzby. Prevodni tabulka je v dokumentu 12. - Validace stromu overuje i konektor. Cizi konektor je chyba, chybejici napojeni nedodelek. ## 2026-08-12 - skripty konektoru Naprogramovana vykonna cast konektoru. Popis je v [11-skripty-konektoru.md](11-skripty-konektoru.md). ### Pridano - `scripts/` se skripty konektoru. Jeden soubor nese manifest (vstupni a vystupni parametry) i kod. Obycejny JavaScript, aby se nemusel prekladat. - Hot reload podle casu zmeny souboru. Uprava v portalu i rucni uprava souboru se projevi bez restartu. - Kontrola vstupu i vystupu proti manifestu, jedna funkce pro obe strany. Chybejici povinny vystup je chyba skriptu, ne uzivatele. - `ctx` predavany skriptu: `http` nad adresou napojeni, `util`, `log`, `config`, `idempotencyKey`, `fail` a `retry`. Skript nedostane pristupove udaje. - Rozliseni opakovatelne a koncove chyby. Runner nikdy nevyhodi vyjimku, vzdy vraci vysledek vcetne `retryable`. - Redakce tajnych hodnot pred zapisem do logu. - Napojeni z environment variables (`src/scripts/connections.ts`) vcetne iDokladu. - Sest ukazkovych skriptu pro iDoklad proti skutecnemu API sluzby `services.csbot.cz/apps/idoklad`, kazdy na jiny vzor. - Stranka `/dashboard/skripty`: seznam, manifest, editor, zkusebni spusteni. Formular testu se sklada z manifestu, nepise se pro kazdy skript. - Endpointy `/api/dashboard/scripts`, `/:id`, `PUT /:id`, `/:id/test` a `/reload`. Vse ve Swaggeru. ### Zmeneno - Katalog konektoru uz neni jen staticky seznam. Akce ze skriptu se domeruji prekryvem v `src/data/connectors.ts`, takze se naraz objevi ve validaci stromu, ve vypoctu scope i v sablonach. Pri stejnem ID operace vyhrava skript. - `ConnectorOperation` ma `implementation` a `scriptId`. Katalog v portalu operace se skriptem oznacuje ikonou. - `ApiError` na klientovi nese cele telo odpovedi a umi z nej vytahnout `issues`. - Dockerfile kopiruje `scripts/` do vysledneho image. ### Vedome neudelano Skripty bezi v procesu serveru, ne v sandboxu. Jsou nase a prosly gitem. Zakaznicke skripty budou potrebovat izolovany engine ve vlastnim vlakne, duvod je v [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md). Ulozeni z portalu zapisuje do souboru v containeru. Bez trvaleho svazku ho redeploy vrati na verzi z gitu. ## 2026-08-12 - navrhy Pridany [09-navrh-rozsireni.md](09-navrh-rozsireni.md) a [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md). 09 popisuje datove modely: akce navazane na typ nebo tag ticketu s telem jako operaci, vlastnim stromem nebo skriptem, typy a tagy ticketu, role a prava jako data misto unionu, zalozky a zpristupneni konektoru za firmu, konektory rozdelene na definici, zpristupneni a napojeni, cekaci krok, sablony zprav, vlastni widgety se seskupovanim a prevod na Postgres. Soucasti je kontrola navrhu proti celemu prikladu se dvema firmami jednoho cloveka. 10 popisuje vykonnou cast: cestu udalosti od webhooku pres inbox a dispatcher k workeru, frontu v Postgresu se `SKIP LOCKED`, davkovy odber, spravedlnost mezi klienty, idempotenci, retence a rozpocet na 150 klientu ve dvou scenarich objemu. Nic z toho neni naprogramovane, oba dokumenty jsou navrh k rozhodnuti. Kod se nemenil. ## 2026-08-03 Tickety predelane na plnohodnotny konektor. Prestavaji byt polozkou v seznamu a stavaji se prichozim pozadavkem, ktery ma sveho cloveka a dohledatelny prubeh. Popis modelu je v [06-tickety.md](06-tickety.md). ### Pridano - Kanaly do ticketu: WhatsApp jako novy konektor, e-mail a hlasova linka jako plnohodnotne spoustece. - Konektor Tickety presunut do nove kategorie `servicedesk`, rozsiren o spoustece `created`, `unknown-customer`, `assigned`, `status-changed` a akce `assign`, `set-status`, `link-customer`. - Resitele (`src/data/people.ts`) oddelene od uzivatelu portalu, spojka e-mailem. - Log ticketu ve strome vcetne toho, co ktera volana sluzba vratila. - Prehled vytizeni tymu, kdo co ma u sebe, zaroven jako filtr seznamu. - Detail ticketu `/dashboard/tickety/:id`: prubeh a log, prirazeni, stav, zakaznik, komentare. - Filtry seznamu ticketu na serveru: resitel (vcetne `me` a `unassigned`), stav, kanal. - `providedFields` v katalogu konektoru: parametry, ktere spoustec predava sam. - Udalost `ticket.assigned` na sbernici i v portalu. - Endpointy `/api/dashboard/people`, `/tickets/workload`, `/tickets/:id` a POST varianty pro assign, status a comment. Vse ve Swaggeru. ### Zmeneno - `Ticket` ma misto volneho `requester` strukturovaneho `customer` s nullable `id` firmy v CRM, k tomu `channel`, `assignee` jako odkaz na cloveka a `automationId`. - `customer.id === null` je nosna informace, ne chybejici udaj. Prave na ni se pta podminka "mame zakaznika?" ve strome automatizace. - Simulace ticketu bere kanal a prepinac, jestli se zakaznik dohleda. Zakladany ticket dostane cely realisticky log. - Builder ukazuje parametry od sluzby jen ke cteni. Server je pri ulozeni vzdy dosadi z katalogu, a to jeste pred validaci stromu. ### Vedome neudelano Bugs a wishes zustavaji mimo. Vyvojarska agenda ma jiny zivotni cyklus a slucovat ji s tickety by znamenalo, ze ani jedna evidence nefunguje poradne. ### Doplneno pote Puvodni verze mela diru: ticket nemel zadny obsah a krok "Zalozit ticket" nesel nastavit. Slo tedy rict "z WhatsApp udelej ticket", ale ne uz co se ma kam ulozit. - `Ticket.body` a `Ticket.sourceRef`. Predmet je shrnuti, telo je cely text pozadavku. `body` vystaveno i ve spoustecich `created` a `unknown-customer`, takze na obsah ticketu jde udelat podminka v navazne automatizaci. - Nastavitelna pole akci (`OperationField` a `FlowStep.inputs`). Ticket, e-mail a WhatsApp maji skutecna pole misto pouhe napovedy. - Sablony `{{parametr}}` v hodnotach poli (`src/data/templates.ts`) vcetne nabidky parametru, ktera je vklada na pozici kurzoru. - Vyber resitele u akci se plni ze seznamu lidi, ne z rucne psaneho ID. - Nevyplnene povinne pole a odkaz na neexistujici parametr se hlasi jako nedodelek. Nastaveni pole, ktere akce nema, je chyba 400. - Akce bez `inputs` to v builderu napisou primo na karte kroku. ### Vystupy kroku a predvalidace Druha dira: kroky slo vkladat kamkoliv, ale podminka videla jen parametry spoustece. Slo tedy pridat krok "zeptej se CRM", ale ne se vetvit podle toho, co vratil. Bez toho byla predvalidace k nicemu. - `outputFields` v katalogu: co akce vrati dalsim krokum. Ma je "Dohledat firmu" (`customerKnown`, `companyId`, `companyName`), "Zaradit do kategorie", "Zalozit obchodni pripad" i "Zalozit ticket". - Nova akce RAYNET "Dohledat firmu". Nic nezaklada, jen odpovi, jestli odesilatele zname. Presne pro predvalidaci. - `src/data/flowScope.ts` pocita, co je videt v kterem miste stromu. Krok vidi spoustec plus vystupy kroku pred nim. Vetev nepridava nic do sekvence za podminkou, protoze nemusela probehnout. - Builder nabizi v podmince i v polich akce presne ty parametry, ktere v danem miste doopravdy jsou. - Odkaz na parametr, ktery ve strome neni, je chyba 400. Odkaz na parametr, ktery vznika az pozdeji, je nedodelek s radou posunout podminku niz. - Duplicitni jmeno parametru ve scope je nedodelek. V sablone by nesl poznat, ktery se dosadi. ### Kanaly a vzorove automatizace - Konektory Facebook Messenger a Instagram, kanaly `facebook` a `instagram` u ticketu. - Ctyri nove vzorove automatizace v rozdeleni, ktere odpovida zameru: jedna na kanal pro prijem, jedna spolecna pro smerovani na resitele. Prijmove zamerne neprirazuji, smerovani si ticket prevezme a podminkou `assigned neni splneno` neprepise rucni rozhodnuti. ### Firmy a prava Treti a nejvazneji dira: portal nemel zadnou tenanci. Kterykoliv prihlaseny uzivatel videl vsechny tickety vsech firem a cely seznam resitelu, `requireRole` se nikde nevolal. Popis v [07-firmy-a-prava.md](07-firmy-a-prava.md). - Tenant jako hranice viditelnosti. `tenantId` na ticketu, resiteli i automatizaci. - Uzivatel muze patrit do **vic firem**, v kazde s jinou roli (`memberships`). Pristup napric firmami je zvlast jako `platformAdmin`. - Tri pohledy na tickety: `all`, `tenant`, `mine`. Admin mezi nimi prepina, vcetne vyberu firmy. - `src/data/access.ts` jako jedine misto, kde se rozhoduje o pravech. `GET /api/dashboard/access` rekne klientovi, co smi kreslit. - Filtr na firmu je v ulozistich **povinny argument**. Zapomenuty filtr neznamena "vse", ale nezkompiluje se. - Nikdy tise nezuzujeme. Cizi firma vraci 403 nebo 404. - Prirazeni jen v ramci firmy. Prehazovat praci mezi lidmi smi jen admin, agent si smi vzit ticket na sebe. - `requireRole` nahrazen `requirePlatformAdmin`. Prava uvnitr firmy resi `access.ts`, protoze zavisi na tom, ktera firma pozadavek zajima. - Demo ucty pokryvaji vsechny tri situace vcetne cloveka ve dvou firmach. ### Nastavitelny dashboard Prehled byl pevne dany. Ted si ho kazdy sklada sam, popis v [08-dashboard-widgety.md](08-dashboard-widgety.md). - Katalog widgetu na serveru vcetne toho, ktere sirky ktery widget unese. Graf v tretine sloupce se necte, proto se tam ani nenabizi. - Rezim uprav na `/dashboard`: pridani z nabidky, vyber sirky, poradi sipkami, odebrani. Ulozi se az tlacitkem, Zrusit vrati puvodni stav. - Rozlozeni se uklada za **dvojici uzivatel a firma**, ne jen za uzivatele. - Data nacita prehled a rozdava je widgetum. Deset dlazdic tak neznamena deset stejnych dotazu. - Server rozlozeni overuje proti katalogu. Neznamy widget nebo nepodporovana sirka se neulozi, widget zmizely z katalogu se v prehledu ukaze jako chyba, ne ze tise zmizi. ### Zapsano jako otevrene rozhodnuti Vsechny automatizace se stejnym spoustecem se spusti. Doporucene rozdeleni na to nenarazi, ale az se bude psat runtime, musi se to rozhodnout vedome. Varianty a doporuceni v [06-tickety.md](06-tickety.md). ## 2026-07-31 Prvni nasazeni aplikace do repozitare csbot-prototype. Puvodni sablona byla holy Express s endpointy `/` a `/health`. Nahradil ji kompletni web a klientsky portal. ### Pridano - Verejny web: homepage se sekcemi, sluzby, o nas, kontakt s formularem, 404. - Prihlaseni pres JWT s demo ucty. - Klientsky portal: prehled s grafem, tickety, incidenty, automatizace, konektory, nastaveni. - Builder automatizaci: strom akci, spoustec, vetveni podminkou. - Katalog 25 konektoru v 8 kategoriich. - Webhook s registrovanou adresou, token generuje server. - Zivy dashboard pres SSE, vcetne indikatoru spojeni a bublin s udalostmi. - Simulace provozu pod tlacitkem v postrannim menu portalu. - Swagger UI na `/docs` a OpenAPI definice na `/openapi.json`. - Dokumentace ve slozce `documentation/`. - `.gitignore`, ktery drzi `node_modules` a `dist` mimo repozitar. ### Zmeneno oproti sablone - Aplikace prepnuta na ESM (`"type": "module"`) a `module: NodeNext`. - Jeden container obsluhuje API i zbuildovanou React aplikaci z `dist/public`. - Dockerfile buildu je server i web, vysledny image dostava jen `dist`. - Port zustava 3000, naslouchani na `0.0.0.0` beze zmeny. ### Reseni reverse proxy - `ROOT_PATH` se cte z prostredi, nikde neni hardcoded. - Aplikace se mountuje na koren i na prefix, funguje tedy at Caddy prefix odstrani nebo ne. - Server vklada do `index.html` znacku `` a `window.__BASE_PATH__`, aby SPA nasla soubory i na vnorenych cestach. - OpenAPI `servers` obsahuje prefix, takze Swagger Try it out vola spravnou adresu. - Router ma `strict: true`, jinak by se presmerovani `/docs` na `/docs/` zacyklilo. ### Overeno lokalne S `ROOT_PATH=/apps/csbot-prototype`: - `/apps/csbot-prototype/health` i `/health` vraci 200, - `/apps/csbot-prototype/docs` presmeruje na `/docs/`, ta vraci Swagger UI, - `swagger-ui.css` se nacte pres prefix, - OpenAPI `servers` obsahuje `/apps/csbot-prototype`, - `index.html` na vnorene ceste obsahuje spravny ``, - prihlaseni pres prefix vraci token, - neexistujici cesta pod `/api` vraci JSON, ne HTML aplikace. ### Znama omezeni Data jsou v pameti, restart je vrati do vychoziho stavu. Obsah webu je ukazkovy. Ulozeny strom automatizace se nevykonava.