Dodelavka. Tvrzeni "prepinac plati pro cely system" nebylo pravdive:
useApiQuery se na zmenu firmy zeptat umel, ale komponenty volajici
apiFetch primo - EntityAdmin, a tim cele Nastaveni a zalozky Resitele
a Skupiny v Lidech - o zmene nevedely a zustala na nich data predchozi
firmy. Presne ta vada, kvuli ktere se to delalo, jen o patro vedle.
- obsah dashboardu ma key podle vybrane firmy, takze prepnuti prestavi
cely strom komponent. Zadny dotaz nemuze zustat stary, protoze zadna
stara komponenta nezustane. Doplnovat zavislost do kazde komponenty
zvlast je totez zapomenuti, jen o patro niz.
- prepinac je v sidebaru nad navigaci, ne v hlavicce, a vypada jako
ovladaci prvek: ram, ikona firmy, sipka. Predchozi verze mela pruhledny
ram a splyvala s popiskem.
- v hlavicce zustal jen nazev aktivni firmy. Driv tam stalo natvrdo
tenants[0], takze po prepnuti ukazovala porad prvni firmu ze seznamu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
permissionsOf(user) scitalo role pres vsechna clenstvi, takze kdo byl
spravce v jedne firme, jednal jako spravce ve vsech, kam patril. Uzivatel
ve dvou firmach je vzacny, dusledek ne.
- firma je povinny argument u permissionsOf, hasPermission i hasAnyPermission.
Zamerne povinny: kdyby byl nepovinny, prvni volani bez nej by tise vratilo
vsechna prava. Prekladac tim rovnou nasel vsech ctrnact mist, ktera se ptaji.
- kazde misto se pta za tu spravnou firmu: akce nad ticketem za firmu toho
ticketu, uprava zaznamu za firmu toho zaznamu, zalozky a helpdesk za firmu,
kterou ma clovek prepnutou
- cizi firemni role se ignoruje, i kdyby na ni clenstvi odkazovalo. Jinak by
stacilo pripsat si ji do clenstvi v jine firme a prava by se prenesla.
- cache prav ma v klici firmu
- readScope cte z jedne firmy, te prepnute, ne ze vsech. V nastaveni se driv
michaly typy ticketu a role napric firmami bez ohledu na vyber.
Opraveno u toho: systemove role se pri startu srovnaji s kodem
(syncSystemRoles). Vychozi sada se pouzije jen do prazdneho uloziste, takze
nove pravo v katalogu se k uz bezici instalaci nikdy nedostalo - ticket.create
ani obe prava helpdesku by spravci firmy nenabehla a zalozka Helpdesk by se
neobjevila. Meni se jen prava, jen u nasich roli, nazvu se to netyka.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Prepinac byl stav uvnitr stranky Prehled, takze prepnuti na LogiTrans
zmenilo prehled a nic jineho - ostatni stranky volaly API bez tenantId
a server sahnul po vychozi firme. V Lidech tak zustali lide Automie.
- vybrana firma je jedna hodnota pro cely dashboard (web/src/lib/tenant.ts)
a prezije obnoveni stranky
- tenantId doplnuje apiFetch, ne jednotlive stranky. Kdyz si to mela
pridavat kazda stranka sama, vetsina na to zapomnela - a prave to byla
ta chyba. Nedoplnuje se, kdyz cesta uz firmu nese, u scope=all
a mimo /api/dashboard.
- useApiQuery se pri prepnuti zepta znovu, jinak by na strance zustala
cisla predchozi firmy
- prepinac je v hlavicce nad obsahem a nahradil text, ktery ukazoval
natvrdo prvni firmu ze seznamu bez ohledu na vyber
- ulozena firma, do ktere uzivatel uz nepatri, spadne na vychozi
Zjisteno u toho a zapsane do 07-firmy-a-prava.md: prava se scitaji pres
vsechny firmy, neprepocitavaji se podle vybrane. Data se tim neprolomi,
server je filtruje dal, ale tlacitka ukazuji vic, nez by mela.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Katalog srovnany s tim, co opravdu bezi na services.csbot.cz/apps:
trinact sluzeb dostalo pristupove udaje a levne cteci overeni, opravena
appId, ktera nikam nevedla (ppl, microsoft365, transcription), a GA4,
Search Console, Google Ads i Sklik ted stoji na aplikaci analytics,
kazda s vlastnimi udaji. Nove sluzby SAP Business One, Google Workspace
a Meta Ads. K tomu 23 skriptu, ktere s nimi opravdu neco delaji.
OpenAI jako prvni sluzba, ktera nebezi u nas: Service.baseUrl s absolutni
adresou, prepis pres <SLUZBA>_BASE_URL nebo adresu u konektoru, predpona
hlavicky u pole udaju (uzivatel vlepi holy klic, Bearer dopise runtime).
Dotaz na model, nahrani souboru, otazka nad souborem, prepis zvuku.
Skript umi odeslat soubor pres ctx.http.postForm (multipart, obsah Base64).
Sluzba E-mail pres SMTP. Neni to skript, ale vnitrni krok - SMTP neni HTTP.
Konektor nese schranku firmy, krok ma HTML telo, ve kterem se dosazene
hodnoty escapuji (znacky autora sablony jsou zamer, ostre zavorky od
zakaznika ne). Overeni konektoru se prihlasi na server a nic neodesle.
Helpdesk: Ticket.helpdeskSourceId drzi firmu, ktera pozadavek poslala,
vlastnikem zustava ta, ktera ho resi - jinak by ho resitel nemel ve sve
fronte. Komu pozadavek pripadne, urcuje Tenant.helpdeskProviderId.
Zadavatel vidi jen svoje pozadavky a smi k nim pripsat komentar.
Opravy v portalu:
- hlasky o ulozisti a odchozi IP vidi jen spravce platformy
- typ ticketu se v automatizaci vybira ze seznamu firmy, nebo dosadi z dat
- stav ticketu je otevreny naseptavac, ne ciselnik
- ticket jde zalozit rucne, zakaznik u nej neni povinny
- kanal se prejmenoval a parametry u webhooku jsou oznacene jako nepovinne
- srovnane markdown tabulky v cele dokumentaci
Co z teto davky jeste neni: prepinac firmy je porad jen stav uvnitr stranky
Prehled, takze se prepnuti neprojevi v Lidech ani jinde.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Echo sluzba na internetu odpovi verejnou adresu. Jenze volani na vlastni
domenu se otaci zpatky na tentyz stroj a reverse proxy pak vidi neco jineho,
typicky adresu docker bridge. A prave tu porovnava seznam povolenych IP
u sluzeb za toutez proxy, takze verejna adresa muze byt povolena a volani
z containeru presto skonci na 403.
Zmeri se to tak, ze portal zavola svoji vlastni verejnou adresu
(PUBLIC_ORIGIN + ROOT_PATH + /whoami) a precte si, jak k nemu volani doslo.
Kruh sam pres sebe, ale nic jineho tuhle adresu nezjisti: mezi container
a server se tim dostane ta sama proxy, kterou prochazi volani na sousedni
sluzby.
Novy /api/whoami je zamerne bez prihlaseni. Vraci volajicimu jeho vlastni
adresu, tedy nic, co by uz nevedel, stejne jako kterakoliv echo sluzba.
Obe mereni bezi naraz a jsou videt na strance Konektory vedle sebe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kdyz cizi sluzba odmitne pristup, prvni otazka je, z jake adresy se vlastne
vola. Z containeru to videt neni, vidi to az protistrana, takze se zepta echo
sluzby podle EGRESS_IP_URL a vysledek se drzi v pameti po EGRESS_IP_TTL_MS.
Prazdna EGRESS_IP_URL funkci vypne, prepsat ji jde na vlastni echo pod svou
domenou.
Adresa je natvrdo na strance Konektory a u kazdeho odmitnuteho overeni v logu.
Pripojuje se jen u 401 a 403 - jinde nema co rict a nestoji za volani ven.
Neni to tajemstvi: kazda volana sluzba tuhle adresu stejne vidi.
Endpoint egress-ip je registrovany pred GET /:id, jinak by ho router vzal
jako id konektoru.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dialog se vykresloval uvnitr karty konektoru misto pres obrazovku. Samo
position: fixed nestaci: rodic s backdrop-filter (nase .glass, tedy skoro
kazdy panel a karta) je pro fixed potomka containing block. Modal proto jde
portalem do document.body. Tykalo se to vsech dialogu, videt to bylo az
u Logu, ktere jsou v male karte.
K chybe se zapisuji vybrane hlavicky odpovedi: server, via, content-type,
www-authenticate, retry-after, x-request-id, date. Rikaji, kdo odpoved vydal.
Server: Kestrel je sama aplikace, Via: 1.1 Caddy proxy pred ni. U 403 od proxy
byva telo prazdne a bez hlavicek by nezbylo vubec nic. Allowlist, ne vsechno:
Set-Cookie a podobne do zaznamu nepatri.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Duvod od sluzby jde primo do hlasky: z tela odpovedi se vytahne detail,
error_description, message, title, error i seznam missingHeaders. Retezec,
ktery vypada jako JSON, se rozbaluje dal - sluzba iDoklad presne takhle
predava telo od iDokladu samotneho. Kdyz sluzba nenapsala nic, rekne se to.
401 a 403 uz nejsou jedna hlaska. 401 = udaje sluzba dostala a neuznala je.
403 = tvar udaju je v poradku, zakazuje se samo volani.
V kazde hlasce je cela adresa vcetne serveru (ScriptRequestInfo.url),
bez query - v query muze byt tajemstvi. 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 a v odpovedi na test, i kdyz
overeni projde.
Tlacitko Logy na karte konektoru a historie poslednich peti overeni.
Odpoved sluzby dosud 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 /connectors/:id/checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bezny postup nesel poskladat z kroku: najdi podle ICO, kdyz neni zkus mail,
kdyz porad neni zaloz - ID dava jednou jedna vetev a jednou druha, ale
rozsah vystupy z vetvi za podminku nepoustel. Slucovat kvuli tomu hledani
a zakladani do jednoho kroku bylo obejiti nasi chyby, ne reseni.
- Vystup z vetve je za podminkou k dispozici, jen jako nepovinny. Ze muze
chybet, se neztratilo: builder to u pole ukaze a pri behu se dosadi
prazdno.
- Vystup se stejnym jmenem uz z nabidky nemaze ten starsi. Po druhem
hledani kontaktu zmizelo ID z prvniho, tedy to, co je v tu chvili
potreba. Odkaz se jmenem kroku je jednoznacny.
- Duplicitni jmena u vystupu kroku uz nejsou nedodelek. Konflikt zustava
mezi parametry spoustece, kde zadny prefix neni.
- Novy krok Zalozit kontakt. Nic nedohledava, hledani je vlastni krok.
Najit nebo zalozit zustava pro toho, komu staci jistota jednim krokem.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Klikaci pravidla jsou u peti poli rychlejsi, ale u modelu objednavky je jich
dvacet a v tom se necte. Vedle nich proto skript firmy: prevod z A do B
napsany v JS, jeden na zakaznika.
- Skripty se ukladaji do uloziste, ne na disk. Disk je uvnitr kontejneru
a redeploy ho vymaze.
- Krok Transformace dat - Vlastni skript. Vysledek jde dal jako krok.result.
- V logu ticketu je u kroku vstup i vystup. Prave to byl duvod, proc skript
nad pravidly vyhral.
- Zkouska bez ulozeni: v portalu se vlepi skutecne telo a hned je videt, co
z toho leze.
- Skripty jsou v zalozce Akce, vedle definic akci. Obojí je popis toho, co
aplikace ve firme umi, a spravuje to tentyz clovek.
Skript je ciste prevod hodnot: dostane input, vrati objekt. Nema require,
import, process, fetch ani console, bezi nejvys 2 s a vysledek se vejde do
256 kB. node:vm neni bezpecnostni hranice proti nekomu, kdo se chce dostat
ven - je to izolace proti nehode a proti zacykleni.
Pri zkousce se ukazalo, ze casovy limit nepokryval samotny beh: runInContext
jen vyrobil funkci a zavolat ji zvenku znamenalo, ze while (true) uvnitr
zablokovalo proces navzdy. Kod se ted vola uvnitr runInContext.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Odesilatel posila cely model. Objednavka ze Shoptetu ma zanoreni, ceny
v podobjektech a seznam polozek - a dosud sel napojit jen plochy seznam
skalarnich poli, takze items[] neslo pouzit vubec.
- Odkaz v sablone muze byt cesta: {{data.order.billingAddress.city}},
{{data.order.items[0].name}}, {{st_faktura.invoiceId}}. Overuje se prvni
cast odkazu, takze ploche odkazy funguji dal presne jako driv.
- Cele telo je v kontextu i v puvodnim tvaru. Deklarovane parametry maji
pri shode jmen prednost.
- Spoustec si pamatuje ukazku skutecneho tela. Server z ni odvodi model,
tedy seznam cest i s typy, a ten se v krocich klika misto opisovani.
Tlacitko doplni z hodnot v ukazce parametry pro podminky.
- Novy krok Pro kazdou polozku: projde seznam a za kazdou polozku vykona
vnoreny podstrom. Uvnitr je item a index, po skonceni krok.results se
seznamem vysledku. Kazdy vysledek nese i puvodni polozku - radek
objednavky potrebuje jak ID z CRM, tak mnozstvi z puvodnich dat.
Strop 200 polozek, mimo seznam krok selze s tim, co tam misto nej je.
- Prevod Za kazdou polozku seznamu v klikacim editoru mapovani. Engine ho
umel, sel ale napsat jen rucnim JSONem.
- Typograficke uvozovky a sipka z kodu pryc.
Overeno nad skutecnym modelem objednavky: 23 cest vcetne
data.order.items[].unitPrice.withoutVat, sablony s cestou i s indexem,
smycka nad dvema polozkami s posbiranymi vysledky.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Overeno proti serveru: tri shodne POSTy udelaly jednu udalost s pocitadlem 3,
ale krok se do logu porad zapsal trikrat. Zadani bylo, ze opakovani ma byt
informace, ne dalsi radek.
- Krok muze rict quiet a jeho radek se do logu ticketu nezapise.
- Ticket vznika uz s poslanym stavem. Predtim se zalozil s vychozim "Novy"
a hned se prepsal, takze v logu stalo "stav Novy -> completed" u ticketu,
ktery v tom stavu nikdy nebyl. Odtud i to "Novy" ve widgetu.
- Zaznam zmen: celkovy pocet behu se pocita od zavedeni historie po dnech,
puvodni citac se den ode dne nedelil a rozpocitat ho zpetne neni z ceho.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nalezeno na bezicim serveru: TK-4946 mel 177 udalosti a 620 radku logu,
pritom se skoro nic nestalo. Zmereno proti fronte: ve stejnem okne vzniklo
presne tolik behu, kolik prislo udalosti (22 a 22), kazdy s jednim pokusem.
Fronta nenasobi nic, odesilatel poslal 177 POSTu. Nase vina byla, ze to
z historie neslo poznat.
- Data udalosti se ukladaji. Kdyz krok nema vlastni, ulozi se to, cim beh
zacal - u webhooku prijate telo. Prazdna udalost je horsi nez zadna.
- Shodna udalost se pocita (repeats, lastAt), nezaklada dalsi radek. Ticket
se pritom nemeni, takze duplikat nerozblika dashboard ani nespusti
automatizaci na zmenu ticketu. Zahodit ji nejde, jinak by nikdo nezjistil,
ze proti nam neco tluce.
- Zmeny se radi pod udalost, ktera je zpusobila, a u udalosti stoji jmeno
automatizace. Log se cte jako "prislo tohle -> zmenilo to tohle".
- Poznamka o stavu jen kdyz se stav zmenil. "z in-progress na in-progress"
u kazde zpravy byl zdroj tech 620 radku.
- runsToday konecne znamena dnes: behy po dnech, k tomu vcera a celkem.
Dosud to byl citac od zalozeni automatizace, jen se jmenoval "dnes".
Vedle toho prace, o kterou slo predtim:
- Prevzeti ticketu ze skupiny (POST /tickets/:id/claim) a krok Predat skupine
s prepinacem automatickeho prideleni nejvolnejsimu.
- Pozvanky do firmy: odkaz s nahodnym kodem, heslo si nastavi pozvany.
- Resitele, skupiny a pozvanky presunuty z Nastaveni do zalozky Lide, cleny
skupiny se vybiraji klikanim.
- Ctyri AI znaky, ktere zbyvaly v kodu, pryc.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`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 s trema stavy:
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 a podminka ve strome se na nej muze zeptat.
Overeno 8 kontrolami: ticket se stavem completed neni automaticky vyrizeny,
dokud to nekdo nerekne. Automatizace s podminkou status = completed zabere na
ticketu, ktery do toho stavu prejde, ale na uz existujici tickety nesahne -
spousti ji udalost, ne stav.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ciselnik new/open/waiting/resolved je pryc. Tickety chodi z cizich aplikaci,
ktere maji svoje stavy - voicebot posila ringing a completed. Nutit je do nasi
ctverice znamenalo, ze u ticketu svitilo "Novy", i kdyz byl podle odesilatele
davno hotovy.
Misto nej priznak `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 jen matlo.
Vyber stavu v detailu nabizi stavy typu, doporucene a ten, ktery ticket ma
prave ted, aby hodnota z cizi aplikace ze seznamu nezmizela. Filtr v seznamu
nabizi stavy, ktere v datech opravdu jsou.
Overeno 11 kontrolami: ticket z voicebota ma stav ringing, pak in-progress
a completed, completed se pozna jako hotovo a zmizi z fronty, filtr i widget
ukazuji tvoje stavy a rucne jde nastavit i "ceka na zpetne volani".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bez databaze lezi data uvnitr containeru, takze redeploy je smaze a seed je
nasype znovu. Dokud nebude Postgres, resi se to trema vecmi:
Ukazkova data jen se SEED_DEMO=1. Automatizace, tickety a incidenty se uz po
kazdem nasazeni nevraci. Konfigurace 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 ve svem kodu. 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.
Dal:
- `ticket/upsert` umi vsechna pole ticketu: zakaznik, 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.
- Seskupovani widgetu podle faze a dva nove widgety: tickety podle stavu
a podle faze za tento mesic, obojí s proklikem na vyfiltrovany seznam.
- Faze se ukazuje jako stav. Driv byl videt jen nas ctyrprvkovy ciselnik,
coz u ticketu z cizi aplikace nedava smysl. Zivotni cyklus zustava vedle
jako drobny text, protoze se z nej pocitaji statistiky.
Overeno 18 kontrolami proti bezicimu serveru.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Opraveno: `ticket/assign` nemel vykonnou cast, takze krok "Prirad resiteli"
vzdycky selhal hlaskou "operace nema vykonnou cast". Doplnen jako vnitrni krok
vedle prirazeni nejvolnejsimu ze skupiny a prirazeni podle externiho ID.
Ukazkova automatizace "Smerovani ticketu na resitele" je vypnuta. Zapnuta
prebirala tickety, ktere uz nekomu patrily podle skutecne automatizace
zakaznika, a prepsat rucni nebo cizi rozhodnuti je to nejhorsi, co muze
automatizace udelat. Do udaju spoustece zaroven pribylo `assigned`
a `knownCustomer`, aby slo napsat podminku "uz je prirazeny, nesahej na to".
`ticket/upsert` prijima `status`: kdyz hodnota patri mezi nase ctyri stavy,
nastavi stav, jinak se ulozi jako faze. Cizi aplikace posila svoje stavy
hovoru a nas zivotni cyklus je pevny, protoze se z nej pocitaji statistiky.
Do shrnuti kroku se napise, co se stalo.
Overeno pripadem z provozu: 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 vcetne
prokliku na vyfiltrovany seznam.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 pri timeoutu.
Fronta ma opakovani s rostouci prodlevou (30 s, 2 min, 10 min, hodina),
spravedlive poradi po firmach (jedna firma s tisicem udalosti nezablokuje
ostatni), navrat zaseknutych behu po restartu a uklid hotovych. Marna chyba
se neopakuje - chybejici skript za minutu existovat nezacne.
Tri druhy spoustecu: push (webhook), vnitrni udalost (vznik a zmena ticketu)
a pull, tedy pravidelne dotazovani u sluzeb bez webhooku (posta, zpravy).
Planovac jen rekne "je cas", samotny dotaz je prvni krok stromu, takze ma
zaznam v logu a opakuje se pri chybe jako cokoliv jineho.
Kontrakt tela webhooku: kazdy parametr ma cestu (data.order.id,
errors.0.message), takze jde napojit i odesilatel s vnorenym modelem.
U adresy je metoda, ukazka tela a kopiruje se cela adresa vcetne domeny.
Vnitrni kroky, ktere sahaji do naseho uloziste: ticket/upsert (zaloz nebo
dopln podle externiho ID), assign-least-busy, assign-by-external, set-type,
set-stage, add-tags, set-status, incident/create, flow/pause a flow/log.
Faze ticketu 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: voicebot posle voicebotId a ticket skonci
u toho, komu patri. Vazba je na jednom miste, ne v kazde automatizaci.
Kazda chyba zaklada incident 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 vidi jen spravce platformy.
Ochrana proti smycce: automatizace navazana na zmenu ticketu ticket meni,
cimz se spousti znovu - pri vyvoji to server polozilo. Resi to oznaceni behu
pres AsyncLocalStorage a strop peti behu na jeden ticket za minutu.
Upozorneni pri prideleni prace vcetne cisla u zalozky Tickety. Zivy dashboard:
dlazdice nad nasimi daty na udalost, data z konektoru podle ttlSec s moznosti
vynutit nacteni znovu.
Opraveno: path a intervalSec u spoustece se pri ulozeni zahazovaly; nad
seznamem neslo pouzit contains, takze na stitky neslo postavit podminku;
novejsi vystup kroku ted prekryje starsi misto hlaseni konfliktu.
Overeno dvema scenari proti bezicimu serveru, 34 kontrol: firma se skladem,
expedici a IT, a hovory z voicebota (callSid do externiho ID, status do faze,
prirazeni podle voicebotId, tri zpravy = jeden ticket se tremi udalostmi).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Runtime: `src/runtime/executor.ts` jde krok po kroku, u podminky se vetvi,
do poli dosadi parametry, akci pusti pres runScript a vystupy pripise do
kontextu pro dalsi krok. Cely prubeh jde do logu ticketu vcetne toho, co
sluzba vratila. Pouzivaji ho obe cesty: akce na ticketu i webhook.
Opraveno: rozlozeni dashboardu s vlastnim widgetem se NEDALO ULOZIT.
`validateLayout` znala jen vestaveny katalog, takze kazdy pokus skoncil
hlaskou "widget v katalogu neexistuje" - presne to, co hlasil uzivatel.
Katalog je ted jedna funkce a pouziva ji nabidka i kontrola. Zaroven je
za konkretni firmu, driv slo polozit dlazdici jedne firmy na dashboard druhe.
Prokliky: z widgetu lidi na cloveka, ze seskupeni na vyfiltrovany seznam
ticketu. Odkazy sklada server, protoze on jediny zna filtr widgetu. Seznam
ticketu cte filtr z adresy a umi filtrovat na typ, tag a skupinu.
Tabulky: spolecna `TicketTable` pro seznam i detail osoby. Na mobilu se
neposouva do strany, uzka obrazovka dostane karty. Detail osoby ma velkou
tabulku se zalozkami "ma u sebe" a "vyresil" a prepinacem pohledu.
Odebrano: simulace vcetne tlacitka, dialogu i endpointu. Trojice pohledu
nad tickety - vyber firmy je select, "moje" je prepinac, driv to delalo
totez dvakrat.
Pridan zmereny rozbor kapacity pro 200 firem (19-kapacita-200-firem.md):
soucasny stav to nezvladne, protoze data jsou v pameti a vypis je linearni.
Zmereno na 5 000 ticketech, vcetne toho, co s tim a kolik serveru to chce.
Overeno 7 kontrolami proti bezicimu serveru.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jakakoliv udalost se muze stat ticketem. Prijem je verejny endpoint na firmu
(`POST /webhook/ticket/:token`), takze zalozit ticket jde i bez stavby stromu.
Externi ID je unikatni V RAMCI FIRMY: dalsi zprava se stejnym ID se navesi na
existujici ticket misto zalozeni druheho, a stejne ID u jine firmy je jiny
ticket. Cislo a retezec jsou tentyz klic. Udalosti se drzi cele vcetne
prijatych dat a jdou rozbalit v detailu - je to neco jineho nez log.
Ticket nove nese firstResponseAt, resolvedAt, resolvedById a reopenCount.
Bez nich neslo rict, kdo kolik odbavil ani jak dlouho zakaznik cekal.
`getAgentStats` z toho pocita vykon resitelu vcetne medianovych casu
a vracenych ticketu. Pocet vyresenych sam o sobe odmenuje toho, kdo tickety
zaviral predcasne, proto je vraceni videt vedle nej.
Widgety: klient konecne vola /widget-data. Endpoint existoval, ale nikdo ho
nepouzival, takze vlastni widget hlasil "nepodarilo se zobrazit". Pribyl zdroj
`connector` - co umi zjistit napojena sluzba, jde vytahnout do dlazdice pres
tentyz skript, ktery pouziva krok automatizace. Vysledek se cachuje.
Akce a widgety uz nejsou v nastaveni, maji vlastni zalozku vedle automatizaci.
Telo akce se sklada stromem, ne JSONem v textarei - je to tentyz editor,
jen misto karty spoustece je "spousti clovek tlacitkem na ticketu".
Nova zalozka Lide se seznamem a detailem osoby. Seznam ticketu i lidi ma dva
pohledy, tabulku a dlazdice.
Opraveno: createTicket bral typeId, fields, tags i assigneeGroupId, ale nikdy
je neukladal. Ticket zalozeny s typem zustaval bez typu a bez vlastnich poli.
Dlouhe pomlcky, sipky, vypustky a bullety pryc z celeho projektu.
Overeno 21 kontrolami proti bezicimu serveru v rezimu souboru.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.gitignore mel vzorec `data/`, ktery se shodl i se `src/data/`. Sestnact
zdrojovych souboru tim tise chybelo v gitu vcetne cele slozky
`src/data/store/`. Opraveno na `/data/`, stejne v .dockerignore.
Tickety vcetne logu, automatizace, incidenty a rozlozeni dashboardu se po
kazde zmene ukladaji. Pomocnik `withMirror` je opak `withCache`: data se meni
v pameti a zapisuji cela, misto aby se po zapisu znovu nacitala. Citace ID se
pri startu dopocitaji z ulozenych zaznamu, takze novy ticket neprepise stary.
Detail ticketu umi typ, tagy, vlastni pole typu a prehozeni na skupinu.
Nastaveni ma prepnuti spravce na jiny ucet, vychozi jen pro cteni.
Skupiny resitelu chodi spolu s lidmi jednim requestem.
Dokumentace: rejstrik znovupouzitelnych funkci (15), navrh monetizace
a ceny za krok (16), popis nastaveni a prav (17). Doplneny endpointy
do openapi.ts, petice CRUD rout se generuje jednou funkci.
Overeno v rezimu souboru: zmeny prezily tvrde ukonceni procesu a po restartu
byly zpatky vcetne logu ticketu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mockup se k databazi nedostane, takze pribyl treti rezim: JSON soubor. Prezije
restart procesu i containeru, ale ne redeploy - filesystem containeru je
docasny. Je to mezistupen, ne nahrada databaze, a tak je to i napsane v portalu.
| Rezim | Kdy | Restart | Redeploy |
| -------- | --------------------------------- | ------- | -------- |
| postgres | DATABASE_URL i SECRETS_KEY | prezije | prezije |
| file | neni DB, ale je DATA_DIR | prezije | ne |
| memory | ani jedno, nebo nejde zapsat | ne | ne |
Rozhodnuti zustava na jednom miste (src/data/connectorStore.ts).
Pridano:
- src/data/snapshot.ts: atomicky zapis (.tmp a prejmenovani), slucovani zapisu
a dokonceni rozepsaneho zapisu pri SIGTERM. Bez atomickeho zapisu by pad
uprostred nechal polovicni JSON, ktery se pri startu nenacte. Rozbity soubor
se prejmenuje na .broken a jede se dal - 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 s pravy
0600, takze sifrovani funguje bez nastaveni. Chrani to proti nahodnemu
precteni JSONu, ne proti pristupu k disku - klic lezi vedle dat a je to tak
napsane i v portalu. U databaze se negeneruje vubec: 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, soubor a databaze
Overeno bez databaze: konektor s vyplnenymi udaji prezil restart, v JSONu jsou
hodnoty sifrovane a plaintext v nem neni. Pote s databazi: rezim postgres
funguje dal a klic vedle dat se nevygeneroval. Kontejner i data/ po overeni
smazany.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pristupove udaje konektoru se ukladaji do databaze a prezijou restart. Popis
v documentation/14-databaze.md.
Databaze je volitelna a rezimy jsou oddelene:
- postgres kdyz je DATABASE_URL i SECRETS_KEY
- memory jinak, tedy pri nasazenem mockupu a lokalnim vyvoji bez DB
Rozhodnuti je jen na jednom miste (src/data/connectorStore.ts). Nikde jinde se
nezjistuje, jestli databaze je - kdyby se to rozlezlo po kodu, jedno misto by se
zapomnelo a chovalo by se pak jinak nez zbytek.
Chybejici databaze nesmi shodit start: container, ktery nenastartuje, je pro
AppFactory nefunkcni sluzba. Misto toho se do logu napise proc a portal to ukaze
na strance Konektory. Stejne tak kdyz migrace selzou - psat do rozbiteho
schematu je horsi nez neukladat.
Databaze potrebuje oboji. Bez SECRETS_KEY by se udaje ukladaly v plaintextu
a to je horsi nez ztratit je pri restartu: tabulku vidi kazda zaloha a kazdy
dump pri ladeni.
Pridano:
- pool v src/db/pool.ts vcetne transakci a dbFor(tenantId) jako sev pro budouci
oddelenou databazi jednoho klienta
- migrace ze src/db/migrations/*.sql pod pg_advisory_lock, jinak je pri rolling
deployi pusti vsechny instance naraz. Jeden soubor je jedna transakce
- sifrovani 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
- /health/ready s pingem do DB. /health na databazi zamerne nezavisi, kratky
vypadek by jinak vedl k restartovani containeru
- GET /api/dashboard/storage a hlaska v portalu o tom, ze data jsou jen v pameti
- 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.
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, pametovy rezim bez
DATABASE_URL. Kontejner po overeni smazan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Transformace dat ve dvou rezimech plus oprava chyby, kvuli ktere se neukladaly
pristupove udaje konektoru. Popis v documentation/13-transformace-dat.md.
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
Dva rezimy transformace, oba nad enginem v src/scripts/mapping.ts:
- transform.map-fields: pole na pole s prevody, klikatelne
- transform.to-json: sablona cileveho objektu s ${cesta}
Marker ${...} je zamerne jiny nez {{...}}. Sablony kroku se dosazuji driv, nez
krok bezi, takze {{total}} by strom stihl vyhodnotit, nenasel by parametr toho
jmena a dosadil by prazdno. Cely retezec navic zachova typ, takze
"unitPrice": "${total}" vyrobi cislo - jinak by cizi sluzba dostala castku jako
text a odmitla ji.
Prevod map pro seznamy je to, bez ceho by priklad nesel dokoncit. Bez nej jde
prevest hlavicku dokladu, ale ne polozky objednavky, a doklad by byl na nulu.
Dal pridano:
- idoklad.create-invoice-from-object: druha polovina prikladu, bere hotove telo
dokladu z transformace a doplni povinna pole ze vzoru iDokladu
- spoustec e-shopu predava celou objednavku jako objekt a polozky jako seznam
- klikaci editor pravidel vcetne rezimu JSON pro vnorena pravidla u map
- 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, overeno
volanim POST i PATCH. 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.
Overeno: npm run typecheck prochazi na serveru i webu, node --check na skriptech.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Slovo "konektor" v kodu znamenalo katalog toho, co umime. Ted znamena napojeni
jedne firmy, tedy to, co tim mysli i uzivatel. Popis modelu je
v documentation/12-sluzby-a-konektory.md.
Tri vrstvy:
- Sluzba: ze iDoklad existuje, co umi a co potrebuje k napojeni. Nase.
- Skript: kod, ktery jednu operaci sluzby opravdu vykona. Nas.
- Konektor: ucet firmy vcetne jejich pristupovych udaju. Firemni.
Pristupove udaje se prestaly cist z environment variables. Cela instance by
mela jedny udaje spolecne a dve firmy by fakturovaly z jednoho uctu. Napojeni
je vlastnost firmy, ne prostredi. Z prostredi zustava jen SERVICES_BASE_URL.
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, zapis do logu
- viditelnost sluzby: vsichni, jen uvedene firmy a lide, nebo jen spravce
platformy. Neviditelna sluzba se z API nevraci vubec, ne se stavem 403 -
firma nema poznat, ze takova sluzba existuje
- src/data/connectorStore.ts: konektory za firmu vcetne hodnot udaju. Hodnoty
se z API nikdy nevraci, jen filled a missing. Prazdne pole hodnotu nemeni,
takze ulozeni formularu bez tajnych hodnot nic nepresepe
- FlowStep.connectorId: krok rika, pod kterym napojenim volat. null = vychozi
konektor firmy, diky tomu je vzorovy strom prenositelny mezi firmami
- overeni konektoru pres verifyPath, tedy cteci volani vyzadujici autorizaci.
U sluzby bez nej se overi jen dostupnost a odpoved to rekne nahlas, jinak by
zeleny vysledek uzivateli lhal
- stranky /dashboard/sluzby a /dashboard/konektory vcetne formularu udaju
- endpointy /api/dashboard/services a CRUD /api/dashboard/connectors ve Swaggeru
- predvyplnene prihlaseni spravcem platformy a prepinac demo uctu na login
strance, kvuli testovani prototypu
Zmeneno:
- stav "napojeno" se prestal cist z katalogu a zacal pocitat z konektoru firmy.
Sluzba ma jen available nebo planned
- validace stromu overuje i konektor. Cizi konektor je chyba, chybejici
napojeni nedodelek - rozdelana prace se nezahazuje
- 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
Overeno: npm run typecheck prochazi na serveru i webu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Konektory dostaly vykonnou cast. Jeden skript je jeden soubor, ktery nese
manifest (vstupni a vystupni parametry) i kod. Diky manifestu s nim umi
pracovat strom automatizace, aniz by o kodu cokoliv vedel.
Soubory jsou zamerne obycejny JavaScript, ne TypeScript. TypeScript by se
musel prelozit a to je presne to otaceni, ktere tady nema byt. Registr
sleduje cas zmeny souboru, takze uprava v portalu, rucni uprava souboru
i novy soubor ve slozce funguji stejne a bez restartu.
Pridano:
- scripts/ se skripty konektoru, nazev souboru je zaroven ID operace
- kontrola vstupu i vystupu proti manifestu, jedna funkce pro obe strany.
Chybejici povinny vystup je chyba skriptu, ne uzivatele - jinak by strom
veril parametru, ktery nikdy nedosel
- 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. Cizi API rado vraci prijaty
token v chybove zprave a log ticketu vidi klient
- napojeni z environment variables 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 vcetne 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 vyhrava skript
- ConnectorOperation ma implementation a scriptId
- ApiError na klientovi nese cele telo odpovedi a umi z nej vytahnout issues
- Dockerfile kopiruje scripts/ do vysledneho image
Ukladani nemuze rozbit fungujici skript: kod se nejdriv zapise do docasneho
souboru, ten se nacte a overi, a az pak prepise puvodni.
K tomu tri dokumenty navrhu dalsich kroku: 09 datove modely a prava,
10 runtime a rozpocet na 150 klientu, 11 popis skriptu konektoru.
Overeno: npm run typecheck prochazi na serveru i webu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Portal nemel zadnou tenanci. Kterykoliv prihlaseny uzivatel videl vsechny
tickety vsech firem i cely seznam resitelu, requireRole se nikde nevolal.
Tenant je hranice viditelnosti, tenantId na ticketu, resiteli i automatizaci.
Uzivatel muze patrit do vic firem, v kazde s jinou roli. Pristup napric firmami
je zvlast jako platformAdmin.
Tri pohledy na tickety: all, tenant, mine. Admin mezi nimi prepina vcetne
vyberu firmy. O pravech rozhoduje jedine data/access.ts, klient si nic
nedovozuje a bere je z GET /api/dashboard/access.
Filtr na firmu je v ulozistich povinny argument, takze zapomenuty filtr
neznamena vse, ale nezkompiluje se. Cizi firma vraci 403 nebo 404, nikdy
tise zuzeny vysledek.
Prirazeni jen v ramci firmy. Prehazovat praci mezi lidmi smi jen admin,
agent si smi vzit ticket na sebe.
Zmena prihlasovani: ucet klient@firma.cz zanikl, demo ucty jsou nove.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ticket dostal telo (body) a odkaz na zdrojovou zpravu. Predmet je shrnuti,
telo je cely text pozadavku.
Akce maji nastavitelna pole (inputs) se sablonami {{parametr}}. Zatim ticket,
kanaly, CRM a AI, ostatni maji jen napovedu.
Krok vidi parametry spoustece plus vystupy kroku pred nim, takze jde vlozit
predvalidaci a vetvit se podle jejiho vysledku. Vetev podminky nepridava nic
do sekvence za podminkou.
Nove konektory Facebook Messenger a Instagram, nova akce RAYNET Dohledat firmu.
Ctyri vzorove automatizace v rozdeleni jedna na kanal pro prijem
a jedna spolecna pro smerovani na resitele.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Web a portal Automia v jednom containeru. Express obsluhuje API
i zbuildovanou React aplikaci z dist/public.
Obsah:
- verejny web: homepage, sluzby, o nas, kontakt, 404
- prihlaseni pres JWT, demo ucty
- portal: prehled s grafem, tickety, incidenty, automatizace, konektory
- builder automatizaci: strom akci, vetveni podminkou
- katalog 25 konektoru v 8 kategoriich
- webhook s registrovanou adresou, token generuje server
- zivy dashboard pres SSE vcetne simulace provozu
- Swagger UI na /docs a OpenAPI na /openapi.json
Soulad s AGENTS.md:
- ROOT_PATH z prostredi, prefix proxy nikde nehardcodovan
- mount na koren i na prefix, funguje s handle_path i bez nej
- base tag a window.__BASE_PATH__ vkladane do index.html za behu
- OpenAPI servers obsahuje prefix, Try it out vola spravnou adresu
- povinne /health a /docs, port 3000, naslouchani na 0.0.0.0
- secrets jen z environment variables, nikdy v logu
Dokumentace ve slozce documentation/.