Files
csbot-prototype/documentation/99-zmeny.md
T
JiriUhlirandClaude Fable 5.1 6eed909a0d Novy ucet videl na dashboardu chybejici widget
Katalog widgetu schovava list.myTickets tomu, kdo ve firme neni veden
jako resitel, ale vychozi rozlozeni ho obsahovalo vzdy. Novy ucet nebo
nova firma tak videly jako prvni vec hlasku "Widget list.myTickets uz
v katalogu neni". getLayout a resetLayout dostavaji stejne hasPerson
jako katalog; bez resitele je misto nej list.unassigned pres celou sirku.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:19:04 +02:00

139 KiB
Raw Blame History

99 - Zaznam zmen

Nejnovejsi nahore.

2026-09-09 - Novy ucet videl na dashboardu chybejici widget

Kdo se prihlasil do nove firmy (nebo jako nove zalozeny ucet), videl jako prvni vec na prehledu hlasku "Widget list.myTickets uz v katalogu neni". Katalog widgetu tenhle widget schovava tomu, kdo ve firme neni veden jako resitel (nema co ukazat), ale vychozi rozlozeni ho obsahovalo vzdy. Dve pravidla o jedne veci na dvou mistech.

getLayout a resetLayout v dashboardLayouts.ts dostavaji stejne hasPerson jako katalog a vychozi rozlozeni bez resitele list.myTickets nema; misto nej jsou nezarazene tickety pres celou sirku. Ulozene rozlozeni se nemeni - kdyz nekomu resitele zrusi, hlaska s tlacitkem "Odebrat" je spravna, protoze si ten widget na dashboard dal sam.

2026-09-09 - Revize projektu: prava, vykon, runtime, portal a ARES

Velka sada oprav napric celym projektem. Zadna nova obrazovka, ale skoro kazda vrstva se zmenila v tom, co dela pri zatezi a pri chybe. K tomu jedna nova funkce: zalozeni firmy podle registru ARES. Zaznam je dlouhy schvalne - tohle je misto, kde se za pul roku hleda, proc se neco chova tak, jak se chova.

Uloziste: jeden rezim pro vsechno

Rozhodnuti o rezimu (postgres, file, memory) delal connectorStore.ts pro konektory a initStores pro zbytek, kazdy podle svych podminek. Mohlo se stat, ze konektory jely z databaze a tickety ze souboru. Ted rozhoduje jen initStores v src/data/store/index.ts: Postgres jen kdyz je DATABASE_URL, migrace prosly a je cim sifrovat (SECRETS_KEY), jinak soubor nebo pamet pro vsechna uloziste vcetne konektoru. connectorStore uz jen vola initStores.

Dalsi opravy v ulozisti, kazda ma za sebou konkretni problem:

Co Proc
audit se maze davkou (removeMany) orezavani mazalo jen radky platformy, audit firem rostl donekonecna. Bezi po 50 zapisech nebo nejvys jednou za minutu
markRead pres updateMany notifikace delaly jeden zapis a jednu obnovu cache na kazdy zaznam
persist / flushPersist u ticketu vic zmen tehoz ticketu v jednom tiku je jeden zapis, ne pet
withMirror radi zapisy za sebou dva put tehoz zaznamu mohl Postgres potvrdit v opacnem poradi a v tabulce zustala starsi verze. Ted je na kazde ID retez promise
issues a stepCount ulozene listAutomations validoval vsechny stromy pri kazdem cteni. Ted se spocitaji pri ulozeni (withDerived) a jednou pri startu
registr cache (refreshCache(kind)) route nastaveni obnovovala vsechny cache, ted jen tu entitu, do ktere psala (bootstrapDataRefresh(route))
listByTenant(tenantIds, sortBy) sedm kopii filtr + razeni v modulech entit
mapy misto find withCache.byId je Map, ticketStore ma ticketsById a ticketsByExternal, vytizeni a statistiky se seskupi jednim pruchodem
create v lokalnim ulozisti hazi na duplicitu Postgres to delal, soubor tise prepsal
snapshot.ts prepise soubor jen pri ENOENT jina chyba cteni (prava, plny disk) driv znamenala start s prazdnymi daty a prepsani souboru prazdnym obsahem. Ted se zapisy zamknou a zaloguje se to
listIncidents(tenantIds) povinne stejne pravidlo jako u ticketu, incident byl posledni seznam bez filtru

Slovnik stavu ticketu je sjednoceny na cesky defaultStatuses (Novy, V reseni, Ceka na klienta, Vyreseno) a ukazkove widgety filtruji closed: false, ne podle nazvu stavu. Ukazkove tickety TK-4817 a TK-4812 vznikaji jen se SEED_DEMO=1. V services.ts byla dvakrat operace set-status, druha tise prekryvala prvni; checkOperationIds() to ted pri nacteni zaloguje.

Spolecne pomocne funkce, aby se nepsaly po modulech: nowIso, minutesAgo, highestNumber, writableOrWarn v store/types.ts, mergeValues v connectors/types.ts.

Runtime: worker je pool a opakuje se jen to, co muze pominout

Worker bral davku ctyr behu a cekal, az dobehnou vsechny. Jeden pomaly beh tak blokoval tri volne sloty. A beh delsi nez deset minut se povazoval za zaseknuty, vratil se do fronty a vykonal se podruhe. Ted:

  • active je mnozina bezicich ID, kazde kolo si vezme CONCURRENCY - active.size behu a spusti je bez cekani na ostatni,
  • claimBatch(limit, active) preskakuje to, co uz bezi,
  • beh kazdou minutu posle tlukot (touchClaim) a za zaseknuty se povazuje az 30 minut od posledniho tlukotu (STUCK_AFTER_MS), ne od vzeti z fronty.

Opakovani. Kazda chyba skriptu se opakovala petkrat za 72 minut, i 403 a spatny vstup. Ted se krok opakuje jen kdyz sam rekne retryable: chyba spojeni, timeout, 5xx a 429, vypadek uloziste konektoru. 401, 403, 404, validace a ctx.fail konci hned a zakladaji incident. U MCP jsou opakovatelne chyby spojeni a 408, 425, 429, 502, 503, 504; timeout uz odeslaneho tools/call opakovatelny neni, protoze MCP nema idempotencni klic a nastroj by se provedl podruhe. Pravidlo je v komentari nad StepResult v executor.ts, aby ho nasel kazdy, kdo pise novy druh kroku.

Dalsi zmeny v behu:

Co Proc
MAX_ACTIONS = 1000 MAX_STEPS = 50 pocita staticky strom. Smycka se dvema kroky nad 26 polozkami narazila na 50 a beh spadl. Vykonane kroky maji vlastni strop
datum v podmince pres Date.parse gt a lt nad datem prevadely ISO retezec cislem, vyslo NaN a podminka byla vzdycky nesplnena
publishOutputs() spolecne vestavene kroky a skripty publikovaly vystupy kazdy jinak. Hole jmeno se zapise jen kdyz v kontextu jeste neni - nastroj MCP vracejici status prepisoval status spoustece
sandbox stavi utils i input uvnitr vm funkce hostitele prosakovaly do skriptu firmy a constructor('return process') z nich utekl ven. Zkompilovane skripty se cachuji (LRU 100)
chybejici nebo pozastavena automatizace neopakovatelna chyba plus incident, driv se to zkouselo dokola
incident/create nese tenantId krok zakladal globalni incident, ktery videly vsechny firmy
planovac zarazuje s triggerem poll bylo manual, takze se v behu nedalo poznat, ze to spustil planovac

Sit a tajemstvi

Novy src/net/guard.ts sdruzuje to, co melo kazde volani ven zvlast: assertAllowedUrl (zakaz vnitrni site), describeFetchError, readBodyLimited a readJsonLimited. Telo se cte proudem a usekne se u limitu - driv se nacetlo cele a teprve pak zmerilo, takze limit nechranil pamet. Pouziva to HTTP skriptu, klient MCP, prihlaseni MCP i SMTP.

Redaktor masky navic maskuje tajemstvi v URL-encoded a JSON-escaped tvaru, protoze cizi sluzby je v chybach vraceji i tak. ctx.config skriptu uz nikdy neobsahuje tajna pole (scriptConfig). EasyWebAuthError dedi z AuthFailure (novy src/mcp/errors.ts), takze se chyby prihlaseni poznaji jednou kontrolou a detail je vzdy zredigovany.

Klient MCP: handshake se cachuje i pro server bez prihlaseni, Mcp-Session-Id se uklada s handshakem a posila znovu, 400 nebo 404 po preskocenem handshaku vyvola jeden novy handshake. OAuth prihlaseni sdili rozdelanou operaci na konektor. V delay() unikal posluchac abortu.

src/scripts/util.ts dostal pick, pickText, jwtExpiry, parseBool, parseNumber a jednu konstantu DETAIL_BYTES na zkracovani (MCP mel 600 znaku, zbytek 8 kB). ctx.util skriptu ma navic day, list, addresses, quote; sablona scripts/_sablona.js je vypisuje a osm skriptu je pouziva.

API: prava se kontroluji za firmu a u kazde route

Prava byla ve vetsine rout jen "je prihlaseny" nebo "je clen firmy". Ted:

Route Kdo smi
firmy CRUD jen spravce platformy
uzivatele CRUD spravce platformy vse. Spravce firmy (user.manage) jen lidi sve firmy, nenastavi platformAdmin, neprida clenstvi jinde, nesahne na spravce platformy a nesmaze cloveka, ktery je i v jine firme
pozvanky user.manage a jen role te firmy
konektory create, update, delete, test connector.manage
automatizace create, update, delete, regenerate automation.edit za firmu automatizace
/services, /connectors/services clenstvi ve firme, cizi firma je 404
assign, status, comment, claim builtinAction v ticketActions.ts, pravo za firmu ticketu a strop viditelnosti (visibleTicketOrDeny)
/api/admin/* impersonate a audit.view se ted opravdu kontroluji

Nove middleware, kazde s jednim ukolem:

Soubor Co
middleware/asyncHandler.ts wrap, safeRouter: odmitnuta promise v handleru driv zabila proces. unhandledRejection se loguje, uncaughtException loguje a ukonci
middleware/rateLimit.ts klouzave okno v pameti: login 20 za 15 min, kontakt 5 za hodinu, prijeti pozvanky 5 za 15 min. 429 s Retry-After
middleware/tenant.ts attachAccess spocita pristup jednou na request do req.access; tenantOrDeny, optionalTenantOrDeny, scopeOrDeny misto kopii v routach
middleware/validation.ts validationError, jeden tvar { error, message, issues: [{ field, message }] }
lib/secure.ts timingSafeEqualString pro tokeny webhooku, prijmu a pozvanek

V index.ts: log requestu maskuje tokeny za /webhook/, /webhook/ticket/ a /invites/; bezpecnostni hlavicky (nosniff, X-Frame-Options SAMEORIGIN, Referrer-Policy, Permissions-Policy); trust proxy = 1, aby limit pocital s adresou klienta a ne proxy; rozpoznani API 404 bere config.rootPath misto natvrdo /apps/. Dockerfile instaluje npm ci.

Vykon: data widgetu nactou seznam ticketu jednou na request, ne za kazdy widget; /tickets a /runs berou limit a offset (nejvys 500) a vraceji X-Total-Count, /tickets i total v tele; hashPassword je asynchronni, aby bcrypt neblokoval smycku; /people/:id prochazi tickety jednou.

GET /api/dashboard/access vraci roleNames, aby klient nehadal popisek role. /storage a /scripts vraceji cesty na serveru jen spravci platformy. V openapi.ts pribylo 22 chybejicich cest, /whoami je opraveny a features uz nejsou popsane jako CRUD.

Udalosti nesou firmu

DashboardEvent.tenantId (null = cela platforma). Stream SSE filtruje zive udalosti i historii podle firem uzivatele, udalost s payload.userId jde jen tomu cloveku. Driv videl kazdy prihlaseny udalosti vsech firem.

Nove udalosti entit tenant|user|role|person|group|ticketType|action|widget|connector|feature s .created|.updated|.deleted, publikuje je crudRouter (volba event), routy konektoru a PUT features. Payload je { id, <druh>: zaznam }, u smazani { id }. Udalosti ticketu ticket.updated, ticket.assigned, ticket.resolved nesou v payload.ticket cely ticket, takze klient opravi seznam na miste a nemusi se ptat znovu.

Portal: obnova bez odmontovani a klientsky sklad ciselniku

Kazda udalost ze streamu odmontovala stranku: useApiQuery prepnul loading a DataState vykreslil spinner misto deti. Ted je loading jen do prvnich dat, potom refreshing, a deti zustavaji. Hooky sdili jeden debounce 150 ms, cache modulu a deduplikaci bezicich dotazu (klic firma + cesta + telo). patchOn opravi data v cache z udalosti (lib/ticketEvents.ts bere payload.ticket). Rozhrani:

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

Rozhodnuti majitele produktu: stredni cesta. Ciselniky (lide, skupiny, typy ticketu, sluzby, konektory, pristup) jsou v klientskem skladu lib/collections.tsx: nacitaji se line pri prvnim pouziti, mazou se pri prepnuti firmy a odhlaseni, opravuji se z udalosti entit (upsert nebo smazani ze zaznamu v payloadu, jinak jedno nacteni te kolekce). Tickety, behy a statistiky zustavaji dotazy na server se strankovanim - jsou velke a meni se porad. Hooky: useCollection(key), useAccess(), useCollectionSelector.

Dalsi opravy klienta:

Co Proc
401 maze token a vyvola auth:expired po vyprseni tokenu portal ukazoval prazdne stranky. Login rekne "Prihlaseni vyprselo", stream se prestane pripojovat na 401 a 403
restore() maze token jen na 401 vypadek site pri startu odhlasoval
onClose modalu v ref fokus se pri kazdem prekresleni vracel na zacatek
toast ma jeden casovac dva toasty za sebou si rusily odpocet
zrusene asynchronni efekty odpoved pro uz odmontovanou stranku prepisovala stav te nove
tiche catch nahrazene chybou pravidlo "zadna ticha selhani" platilo na serveru, na klientovi ne vsude
filtry ticketu v URL nalez slo poslat kolegovi a vratit se pres zpet
detail ticketu neblokuje chyba /people jeden padly dotaz na ciselnik schoval cely ticket
MappingEditor stabilni klice radku smazani radku prekreslilo vsechny nasledujici a ztratil se kurzor
lib/useUnsavedChanges.ts odchod z rozepsaneho builderu bez varovani
DashboardLayout lazy, sourcemapy vypnute verejny web nenacital kod portalu, produkce neposila zdrojaky
TicketTable tabulka nebo karty useMediaQuery misto duplicitni komponenty

Builder. collectScopes memoizovane, karty v memo, callbacky podle ID kroku; FlowCanvas je rozdeleny do flow/{ActionCard,ConditionCard,ForeachCard,StepControls}.tsx a canvasTypes.ts. Stranka Konektory je rozdelena do pages/dashboard/connectors/{ConnectorCard,ConnectorEditor,ConnectorLogs,ConnectorTools}.tsx. Pred tim byl kazdy stisk klavesy ve strome o padesati krocich prekresleni vseho.

Formularova vrstva podle navrhu v dokumentu 25, sekce 2, je hotova: components/ui/form/{controlClass,Field,Input,Select,Textarea}.tsx, lib/useSubmit.ts, lib/options.ts, components/ui/Chip.tsx, components/dashboard/TicketCard.tsx (kompaktni varianta), plural() v lib/format.ts. Zmizelo 15 kopii trid vstupniho pole.

Jazyky. Verejne stranky (Postup, Produkty, Reference, O nas, Kontakt, 404, Prihlaseni, Sluzby, paticka, navigace) plus DataState a ErrorBoundary jdou pres i18n a en.ts je pro ne uplna.

Sdilene typy: ciste typove moduly v src/shared/*.ts (16 souboru, vcetne users.ts pro ucet a clenstvi) jsou jediny zdroj typu API. web/src/types/dashboard.ts, events.ts i AuthContext je re-exportuji pres alias @shared/* (web/tsconfig.json paths a vite.config.ts alias). Pri prevodu se nasly rozjete tvary, vsechny vyresene ve prospech serveru: webovy Ticket nemel createdById, Access.roleNames bylo nepovinne, Person nemel enabled, Incident nemel tenantId ani source, Service neznal kategorii transformace, seznam operatoru podminky u typu list na webu nemel contains, takze builder nenabizel podminku nad stitky, ktera na serveru funguje. Serverovy ulozeny tvar (StoredTicket, Connector s values) zustava na serveru; web dostava PublicConnector jako Connector. Pravidlo od ted: novy typ odpovedi patri do src/shared, web ho nekopiruje.

Nova funkce: firma z registru ARES

Zalozit firmu znamenalo opsat nazev, IC, DIC a adresu rucne a pak zvlast zakladat ucty. Ted je to na /api/dashboard/settings/ares, jen pro spravce platformy:

Endpoint Co
GET ares/companies?query= podle IC presne, jinak podle nazvu. U firmy, ktera uz v portalu je, vraci existingTenantId
GET ares/companies/{ico}/persons soucasni clenove statutarniho organu a prokura z verejneho rejstriku, kazdy s navrzenym e-mailem IC-poradi@placeholder.cz
POST ares/tenants zalozi firmu s ico, dic, address, legalForm z ARES a ucty vybranych osob, vsechny s roli role_admin v nove firme, nahodne heslo, audit tenant.create.ares

Zaznam firmy ma nove nepovinne ico, dic, address, legalForm; CRUD firem hlida unikatni IC. Adresa registru je ARES_BASE_URL, vychozi https://ares.gov.cz/ekonomicke-subjekty-v-be/rest. Klient ARES pouziva tentyz net/guard.ts jako vsechno ostatni, co vola ven.

Rozhodnuti o rolich: spravce platformy zaklada firmy, spravce firmy pak spravuje skupiny, vedouci a cleny uvnitr firmy. Rejstrik nezna e-maily, proto zastupne adresy - spravce je musi nahradit skutecnymi, jinak se ti lide neprihlasi a nedostanou pozvanku.

2026-09-08 - Otevrena stranka sekala prehravani videa

Pri otevrenem portalu zacalo vedle nej sekat prehravani videa, po zavreni stranky bylo hned dobre. Neslo o smycku v kodu: zivy prenos udalosti je SSE, jedno spojeni na celou aplikaci, a setTimeout je v nem jen na odstup pri znovupripojeni. Zdrzeni bylo jinde a byly to tri veci.

Rozmazavani plne barvy

.glass, tedy podklad skoro kazdeho panelu, mel backdrop-filter: blur(14px). Jenze pod kartami je plna barva (bg-ink-950 na dashboardu), takze se rozmazavala jednolita plocha: zadny rozdil videt nebyl a kazda karta si za to drzela vlastni rozmazanou vrstvu ve skladaci. V kodu je glass na 58 mistech, na jedne obrazovce jich je snadno dvacet.

Rozmazani zustava tam, kde pod nim neco opravdu projizdi: lepici hlavicka dashboardu i webu, podklad modalu a prekryv menu na mobilu. To je pet mist, kazde se svym backdrop-blur-* primo u sebe.

Nekonecne animace nad velkymi rozmazanymi plochami

Prihlaseni a verejny web meli zare o velikosti 26 az 34 rem s blur(120px), kterym se donekonecna animovala pruhlednost. Animovat pruhlednost silne rozmazaneho objektu znamena prepocitavat obrovskou vrstvu sedesatkrat za sekundu, porad, i kdyz se na strance nic nedeje.

Zare zustaly, animace zmizela. Krome toho pribylo prefers-reduced-motion: kdo si v systemu animace vypnul, nema je dostat ani tady.

Kazda udalost prekreslila cely dashboard

EventStreamProvider mel v tomtez kontextu stav spojeni, odber i seznam poslednich udalosti. Seznam se meni pri kazde udalosti, takze se menila cela hodnota kontextu - a prekreslil se kazdy, kdo si jen zaridil odber. To jsou vsechny dotazy s refetchOn na strance, i kdyz o ten typ udalosti nestaly.

Pri behu automatizace, ktera posila automation.run a ticket.updated, se tak cely dashboard prekresloval nekolikrat za sekundu - a s nim dvacet rozmazanych vrstev.

Kontext je proto rozdeleny: useEventStream da stav a odber, useEventLog seznam udalosti. Seznam si bere jen ten, kdo ho opravdu vypisuje, tedy toasty.

Filtr sekci nefiltroval

Seznam ticketu sklada adresu dotazu v useMemo, ale v zavislostech chybely groupId, typeId, tag a stage. Adresa se tedy po prepnuti sekce neprepocitala a data se nenacetla znovu - filtr vypadal, ze nedela nic.

Driv to nevadilo: byly to filtry, ktere sly nastavit jen odkazem a po nacteni stranky se uz nemenily. Se zalozkou "Vsechny tickety" a jejim vyberem sekce se z toho stala chyba, ktera je videt.

Overeno: groupId=grp_servicedesk vrati jeden ticket, groupId=none zbyle dva.

2026-09-07 - Podminka se muze ptat na vic veci naraz

Podminka byla prave jedna otazka. Slozitejsi vetveni se muselo skladat z vnorenych podminek, takze "vysledek dorazil" a v nem "vysledek je X" byly dve urovne stromu misto jedne vety. U tri hodnot, ktere maji dopadnout stejne, to byly tri urovne, ve kterych se nikdo nevyzna.

Model

rules: ConditionRule[]     // otazky
match: 'all' | 'any'       // a zaroven / nebo

Stara podoba (fieldId, operator, value primo na kroku) se dal cte, prevadi ji rulesOf - jedine misto, kde se to deje. Kdyby se fieldId cetlo primo, krok ulozeny driv by po zmene modelu prisel o svou otazku a vetvil by vzdycky stejne, tise a bez chyby. Zapisuje se uz vzdycky rules.

Aby se na zadne cteni nezapomnelo, je fieldId v typu nepovinne. Prekladac tim ukazal vsech pet mist, ktera podminku ctou: vyhodnoceni, obe hlasky do logu, kontrola pri ukladani a kontrola nedodelku.

Proc jedna uroven a ne vyrazy se zavorkami

Dve treti podminek jsou "vsechny tohle" nebo "cokoliv z tohohle". Zavorky by v rozhrani znamenaly editor vyrazu, ktery uz nikdo neuklika, a textovy zapis by navic zahodil to podstatne: odkaz na parametr pres ID. Diky nemu prejmenovani parametru podminku nerozbije a builder umi nabidnout jen operatory, ktere na dany typ sedi, a rovnou rict, ze se odkazuje na parametr, ktery vznika az pozdeji.

Az se ukaze, ze jedna uroven nestaci, da se textovy zapis pridat nad tentyz vyhodnocovac. Opacne to nejde.

V builderu

Radek na otazku, k tomu tlacitko "Přidat otázku" a od druhe otazky prepinac sedí všechny nebo sedí aspoň jedna. U jedne otazky se prepinac nenabizi, nema co spojovat. Posledni otazka nejde smazat: podminka bez otazky by tise nevetvila, proto ji runtime rovnou povazuje za nesplnenou a zaloguje to.

V logu

Radek podminky nese vsechny otazky i s tim, ktera rozhodla:

Podmínka: result isNotEmpty a zároveň result eq Chybějící informace: nesplněno
   result = "Přesměrování": sedí
   result = "Přesměrování", porovnáno s "Chybějící informace": nesedí

Bez rozpadu by u spojene podminky bylo videt jen "nesplneno" a ne to, ktera otazka to zpusobila.

Overeno na bezici instanci

Strom se dvema spojenymi podminkami, pet prichozich volani:

Co prislo Prirazeno Vyrizeny
vysledek nedorazil ne ne
Chybějící informace ano ne
Přesměrování ne ano
Mimo téma ne ano
Něco jiného ne ne

Prvni podminka je all (dorazil a zaroven je to Chybejici informace), druha any (Přesměrování nebo Vyřešeno nebo Mimo téma). Strom ulozeny ve stare podobe se nacte a ulozi beze zmeny.

2026-09-07 - Automatizace zavirala tickety uz pri zvoneni

Na instanci nebyl ani jeden nevyrizeny ticket: vsech 131 melo closed: true, takze dlazdice "Moje tickety" i "Fronta bez resitele" ukazovaly nulu. Widget pocital spravne, spatna byla data.

Cim to bylo

Strom se ptal result neq "Chybějící informace" a ve vetvi ANO ticket zaviral.

Jenze jeden hovor posle vic zprav a ta prvni jen ohlasi, ze zacal: data je null, takze {{result}} je prazdne. evaluate prevadi chybejici hodnotu na prazdny retezec, a prazdno se opravdu nerovna "Chybějící informace" - podminka tedy sedla a ticket se zavrel uz pri zvoneni. Vsechno ostatni, co pak prislo, uz jen doplnovalo zavreny ticket.

Odtud i druha vec: prirazovaly se i tickety, ktere nemely. Jedna zprava hovoru mela result: "Chybějící informace", takze ticket sel na servicedesk a na cloveka. Dalsi zprava tehoz hovoru prinesla "Přesměrování" a ticket zavrela, ale resitele uz neodebrala. Z dvaceti ticketu prirazenych jednomu cloveku jich melo "Chybějící informace" jen deset.

Oprava stromu

Nejdriv se ptame, jestli vysledek vubec prisel, a teprve pak kladne, jestli je to "Chybějící informace":

result isNotEmpty
  ANO  result eq "Chybějící informace"
         ANO  priorita vysoka, sekce Servicedesk, prirazeni nejvolnejsimu
         NE   zavrit
  NE   nic, vysledek jeste nedorazil

Puvodni neq znamenalo "vsechno ostatni vcetne toho, co jeste nevime". To je u nepovinneho parametru, ktery dorazi az pozdejsi zpravou, past.

Aby to slo poznat z logu

Radek podminky rikal jen "splneno". Ted nese i to, s cim se porovnavalo, a rozlisuje nedorazilo od prazdneho:

Podmínka: result isNotEmpty: nesplněno    | result = nedorazilo
Podmínka: result eq Chybějící informace   | result = "Přesměrování", porovnáno s "Chybějící informace"

Prvni radek je presne ta informace, ktera chybela. Bez ni log tvrdil, ze podminka sedi, a nikdo nemel jak zjistit proc.

Overeno prehranim celeho hovoru

Zprava Vysledek
in-progress, bez dat ticket vznikl, neni vyrizeny, bez resitele
completed, Chybějící informace priorita vysoka, prirazen clenu Servicedesku
completed, Přesměrování zavren

Pred opravou byl ticket vyrizeny uz po prvni zprave.

Co zustava k rozhodnuti

Kdyz se hovor prehodnoti z "Chybějící informace" na neco jineho, ticket se zavre, ale resitel na nem zustane. Do "Mych ticketu" uz nespadne, ty ukazuji jen nevyrizene, ale ve vsech ticketech u nej to jmeno stoji. Odebrat resitele pri zavreni by slo, jen na to zatim neni krok - ticket/assign bez resitele skonci chybou.

2026-09-02 - Nazev znacky uz neni v kodu natvrdo

Server mel "Automia" napsanou primo v titulku Swaggeru a v OpenAPI - zbytek po prejmenovani, ktere probehlo jen na klientovi. Prejmenovat produkt tedy znamenalo hledat retezec po souborech.

Dve promenne, a je to zamer

Kde Promenna Kdy se dosadi Vychozi
server BRAND_NAME za behu WorkNuke
klient VITE_BRAND_NAME pri buildu WorkNuke

Klient je staticky soubor, takze runtime promenne containeru do prohlizece nedosahnou - proto se u nej dosazuje pri buildu. Obe maji tutéž vychozi hodnotu, takze bez nastaveni cehokoliv sedi.

Zbytek firemnich udaju (claim, kontakty, adresa) zustava v web/src/config/brand.ts. Do promennych to nepatri, meni se to jednou za rok a env by z toho udelalo deset promennych, ktere nikdo nenastavi.

Co se timhle nemeni

Firma Automia v ukazkovych datech. Pozvanka ukazuje jmeno firmy, do ktere zve, a ta se v seedu jmenuje Automia. Neni to zbytek po prejmenovani, je to zaznam zakaznika - zadna promenna znacky ho nezmeni a nema.

Overeno: s BRAND_NAME=ZkouskaZnacky ma OpenAPI titulek "ZkouskaZnacky - portal a API" a Swagger "ZkouskaZnacky API".

2026-09-02 - "Interni chyba serveru" pri zalozeni ticketu

Formular noveho ticketu koncil na 500 uz pri vyplnenem samotnem predmetu.

Cim to bylo

apiFetch prevadi telo na JSON sam:

body: body !== undefined ? JSON.stringify(body) : undefined,

Dialog mu ho ale predaval uz prevedene, tedy body: JSON.stringify({...}). Druhy prevod z objektu udelal retezec a na server dorazilo "{\"subject\":...}". express.json je ve vychozim nastaveni strict, takze retezec na nejvyssi urovni odmitne a vyhodi vyjimku.

Tatáz chyba byla i ve dvou mistech helpdesku: zalozeni pozadavku a komentar.

A druha polovina: spatna hlaska

Vyjimka z parseru tela propadla do centralniho error handleru, ktery z nej udelal 500 "Interni chyba serveru". Ta hlaska rika, ze je neco spatne u nas, a posila cloveka hledat na spatnou stranu - pritom slo o spatne polozeny dotaz.

Error handler proto rozpozna chyby parseru tela (status 400 a type zacinajici entity.) a vraci 400 s vetou "Telo pozadavku neni platny JSON objekt." Ostatni chyby zustavaji 500, jak byly.

Overeno: telo zakodovane dvakrat vraci 400 s tou vetou, opravene telo jen s predmetem vraci ticket.

2026-09-02 - Builder nabizi jen to, co jde zavolat

V nabidce kroku byly vsechny sluzby katalogu, i ty, ke kterym firma nema napojeni. Slo tedy vybrat Raynet CRM bez konektoru a postavit strom, ktery pri prvnim behu spadne na chybejicich udajich - a to se pozna az za tyden, kdyz prijde prvni ostra udalost.

Pravidlo

Nabizi se sluzba, ktera je obecna, nebo k ni firma ma napojeni.

Obecne jsou ty, co se bez konektoru obejdou: webhook, casovac, rucni spusteni, formular, incident, ticket, HTTP, transformace, pauza a zapis do logu. Ma to uz priznak general, jen ho nikdo nepouzil na filtrovani nabidky.

Katalog se nefiltruje, jen se oznacuje

GET /api/dashboard/services prida ke kazde sluzbe connected, tedy jestli k ni firma ma aspon jedno napojeni. Sluzba z odpovedi nemizi: log ticketu a detail akce podle katalogu prekladaji ID operaci na jmena, a kdyby zmizela, zustalo by v uz zapsanem radku hole ID. Filtruje az builder.

Aby nabidka nevypadala jako cely katalog, je pod ni veta, kolik sluzeb ceka na napojeni. Bez ni to vypada, ze sluzba neexistuje, misto ze k ni chybi udaje.

Soukroma sluzba uz neni videt cizi firme

canSeeService vracelo spravci platformy true driv, nez se vubec podivalo na viditelnost sluzby. Zakazkova integrace omezena na jednoho klienta se tak ukazovala i po prepnuti do jine firmy - spravce si ji mohl vybrat do jeji automatizace.

Rozhoduje firma, ne clovek. Kdyz je firma vybrana, plati jeji seznam; bez vybrane firmy spravce platformy spravuje katalog a vidi vsechno. Zaroven se konecne pouziva tenantHasService, tedy zpristupneni sluzby firme pres nastaveni - dosud to bylo pole, ktere nikdo necetl.

Overeno: Polstryn SAP je pro tnt_logitrans v katalogu, pro tnt_automia uz ne, a to i pro spravce platformy. V nabidce builderu pro tnt_automia zbylo deset obecnych sluzeb plus iDoklad, na ktery firma napojeni ma.

2026-09-02 - Tickety maji zalozky, fronta umi prirazovat

Seznam ticketu mel dva prepinace, "Moje tickety" a "Ve fronte", schovane mezi ostatnimi filtry - splyvaly s nimi a vypadaly jako dalsi dva chipy. Jenze "co mam u sebe", "co jeste nikdo nema" a "co je ve firme" nejsou filtry, jsou to tri ruzne prace a kazda chce jinou tabulku.

Tri zalozky

Zalozka Kdo ji ma Cim se lisi
Moje tickety kdo je veden jako resitel bez sloupce Resi, byl by tam porad on
Nezarazene kazdy radky s rychlym prirazenim
Vsechny tickety kdo vidi i cizi tickety sloupec Resi a filtr na sekce

Sloupec Resi je jen ve Vsech. V Mych by ve vsech radcich stalo tyz jmeno a ve fronte je z definice prazdny. Presne o tom mluvil pozadavek: az v prehledu vsech ma smysl videt, kdo co ma u sebe.

Zalozka Vsechny se nenabizi tomu, kdo vidi jen svoje - byl by to tentyz seznam. Rika to Access.seesOthers, a Access.visibleGroups k tomu prida sekce, ktere smi filtrovat: kdo vidi celou firmu vsechny, vedouci jen ty, ktere vede. Nabidnout mu sekci, ze ktere stejne nic neuvidi, je jen matouci.

Fronta umi prirazovat

Nova QuickAssign: u kazdeho ticketu ve fronte tlacitko Vzit si a dve roletky, komu a do ktere sekce. Prirazeni je ve fronte hlavni prace, ne jedna z mnoha veci na detailu - otevirat kvuli tomu kazdy ticket znamena u dvaceti ticketech ctyricet kliknuti navic.

Dve roletky, ne jedna: clovek a sekce jsou dve ruzna rozhodnuti. "Tohle je pro ucetni" jde rict bez toho, aby se resilo, kdo z nich ma dovolenou.

Kdo nesmi rozdavat praci ostatnim, vidi jen "Vzit si". Endpointy uz existovaly (/claim, /assign, /group), nova je jen cesta k nim.

Vychozi zalozka podle adresy

Odkaz z widgetu nese assignee, takze unassigned otevre frontu a konkretni resitel nebo sekce pohled Vsechny. Bez toho by clovek prisel z dlazdice "fronta bez resitele" a koukal na svoje tickety.

Kdo neni veden jako resitel, nema zalozku Moje. Vychozi stav ji ale predpoklada, protoze prava dorazi az po prvnim vykresleni - proto pojistka, ktera po nacteni prav prepne na prvni dostupnou zalozku.

Hledani s tim pocita

Zapsano do navrhu: hledani ma zapadnout do zalozky, ve ktere clovek stoji, ne ji obejit. Z Mych hleda ve svych, z Nezarazenych ve fronte, z Vsech v celem rozsahu stropu, a vysledek se vraci do te same zalozky. Volba "hledat vsude" dava smysl jen tam, kde zalozka Vsechny vubec je.

Overeno na bezici instanci

Kdo Zalozka Vsechny Sekce ve filtru
spravce platformy ano vsechny
Vomacka, vede Servicedesk ano jen Servicedesk
Kriz, radovy clen ne zadne

A cely pohyb ticketu frontou: admin preda TK-4819 do sekce, Kriz ho vidi ve fronte, vezme si ho a objevi se mu v Mych ticketech.

2026-09-02 - Nezarazene vidi kazdy, a dlazdice "Moje tickety"

Dve veci, ktere ze stropu viditelnosti vypadly.

Nezarazeny ticket je ve stropu vzdycky

Prvni verze ho schovavala: kdo nevidel na celou firmu, nevidel ticket, ktery nema resitele ani skupinu. Byla to diera v provozu. Prichozi ticket, ktery jeste nikdo nesmeroval, nepatri do zadne sekce, takze by ho nevidel nikdo krome vedeni - a nikdo by si ho nevzal. Fronta je spolecna, prave proto je to fronta.

withinVisibility proto pousti kazdy ticket bez resitele, at uz ma skupinu nebo ne. Jakmile si ho nekdo vezme, plati strop jako u kazdeho jineho.

Dlazdice "Moje tickety" a "Fronta bez resitele"

Byly v navrhu od zacatku a nikdy se neudelaly. Presne jak navrh rikal, staci zaznam v katalogu a zdroj v builtinSources - zadna nova komponenta, data pocita tatáz cesta jako u vykonu resitelu.

'list.myTickets':  { kind: 'ticketList', filter: { assignee: ['me'], closed: false }, limit: 8 },
'list.unassigned': { kind: 'ticketList', filter: { assignee: ['unassigned'], closed: false }, limit: 8 },

K tomu tri veci, ktere si to vyzadalo:

  • closed ve WidgetTicketFilter. Bez nej se vyrizene tickety nedaly odfiltrovat: stav je volny retezec a vyjmenovat vsechny podoby slova "hotovo" se neda. Prospeje to i vlastnim widgetum, dosud neslo postavit "otevrene tickety podle typu".
  • ResolvedScope.personId se plni vzdycky, ne jen u pohledu mine. Prehled se pta v pohledu tenant, takze filtr me nemel co dosadit a dlazdice vracela prazdno. Kdo je to "ja", na pohledu nezalezi.
  • Kdo neni veden jako resitel, tomu se "Moje tickety" nenabidnou. Ticket se prirazuje resiteli, ne uctu, takze by takovy clovek koukal na prazdno navzdy a nedozvedel se proc.

Vychozi rozlozeni

Nahore to, co clovek muze udelat, teprve pod tim cisla:

Moje tickety, Fronta bez resitele
Aktivni automatizace, Otevrene tickety, Bezici incidenty
Posledni tickety, Incidenty

Graf behu z vychozi sady ven. Je to nejmene srozumitelna dlazdice pro noveho cloveka - behy ceho a co s tim - a zabira celou sirku. V katalogu zustava.

Zmena se projevi jen tem, kdo si dashboard jeste neupravili. Rozlozeni se uklada za dvojici uzivatel a firma, kdo uz si ho osahal, musi dlazdice pridat rucne nebo dat "Vychozi".

Overeno na bezici instanci

Kriz jako radovy clen sekce: "Moje tickety" vraci jeho TK-4820, "Fronta bez resitele" nezarazeny TK-4819, a v seznamu ticketu vidi oba. Pred opravou personId vracela prvni dlazdice prazdno.

2026-09-02 - Detail ticketu ma rozradovace, ne tri karty pod sebou

Navrh v 25 mel detail jako jednu slozku s rozradovaci, ale v aplikaci zustaly tri karty pod sebou: obsah, prichozi udalosti a log prubehu. Detail byl tri obrazovky dlouhy a to podstatne, tedy obsah pozadavku, se v tom ztracelo.

Ted jsou to listy jedne karty:

Rozradovac Co je na nem
Požadavek obsah a pole na komentar, vychozi
Log průběhu co delala automatizace a co ji sluzby vratily
Události co presne prislo zvenku
Vlastní pole hodnoty poli typu ticketu

U kazdeho je pocet, takze je videt, jestli se tam vubec vyplati kliknout. Vlastni pole se nenabizeji, kdyz zadna nejsou - prazdny rozradovac je jen dalsi vec k prehlednuti.

Vychozi je vzdycky Pozadavek: to je duvod, proc clovek ticket otevrel. Log a udalosti jsou hloubkova data, na ta se sahá, az kdyz neco nesedi.

Vlastni pole se do te doby nedala zobrazit vubec, i kdyz je ticket nesl - to je ten ctvrty rozradovac. Kresli je tatáz tabulka jako obsah ticketu (DataTable v TicketBody.tsx), takze se struktura vsude vypisuje stejne.

Rozradovace pouzivaji tentyz vzhled zalozek jako zbytek portalu, ne tvar slozky z navrhu. Druhy styl zalozek na jedne obrazovce je presne ten drift, ktery jsme uklizeli u vstupnich poli.

2026-09-02 - Hierarchie firmy: kdo co vidi, a zalozka Firma

Pohled na celou firmu dostal kazdy, kdo do ni patril: scopes.push('tenant') se v access.ts neptalo na nic. mine byl dobrovolny filtr, ne strop, takze resitel s roli agent poslal ?scope=tenant a dostal cely provoz.

Rozdil, na kterem to ted stoji: pohled je co chci videt, strop je co vubec smim videt. Existoval jen pohled a klientovi se veril.

Model

Priznak visi na clenstvi, ne na cloveku a ne na skupine. Diky tomu muze byt clovek v peti sekcich a jen ve dvou z nich videt vsechno - to role rict neumi, ta je jedna na celou firmu (Membership je dvojice firma a role).

  • Membership.seesAllTenant - vidi cely provoz firmy. Postaveni uctu ve firme, ne vlastnost resitele: clovek muze byt ve dvou firmach jednou reditel a jednou brigadnik.
  • PersonGroup.members je { personId, seesAll } misto holeho personIds. seesAll je vedouci sekce. Vedoucich muze byt vic a jeden clovek muze vest vic sekci, proto to neni zvlastni pole na skupine.
  • Stara podoba personIds se dal cte, prevadi ji groupMembers - jedine misto, kde se to deje, aby skupina ulozena driv neprisla o cleny.

Vypocet stropu

visibilityFor v data/access.ts, sjednoceni ne prunik:

spravce platformy nebo seesAllTenant  ->  cela firma
jinak                                 ->  moje tickety
                                        + vse ze sekci, kde mam zaskrtnuto
                                        + fronta techhle sekci

seesAllTenant === undefined jsou zaznamy ulozene driv. Tam rozhoduje pravo ticket.assign.others: kdo dosud smel prehazovat cizi praci, uz stejne cely provoz videl, takze se mu nic nebere. Bezny resitel timhle sitem neprojde a spadne na svoje sekce, coz je prave ta zmena, o kterou jde.

Kde se to vynucuje

Strop je povinna soucast TicketFilter, stejne jako tenantIds. Nepovinny filtr na prava je filtr, ktery jednou nekde chybi - takhle prekladac ukaze kazde misto, ktere ho jeste nema. Pri zavedeni jich naslo trinact.

  • listTickets filtruje pres withinVisibility
  • getWorkload a getAgentStats uz nesahaji do pole ticketu primo, jdou pres listTickets. Driv obchazely kazde omezeni viditelnosti
  • getTicket kontroluje strop i u jednoho ticketu. Bez toho by stacilo znat ID: seznam by ho schoval, ale adresa detailu vydala
  • detail a prevzeti pocitaji strop za firmu ticketu, ne za prave prepnutou - odkaz z pohledu "vse" muze vest do jine firmy uzivatele

Helpdesk

Ridi se toutez hierarchii, jen "moje" znamena neco jineho: zadavatel pozadavek nikdy nema prirazeny, resi ho nekdo u dodavatele. Proto ma ticket nove createdById a v helpdesku plati: kdo vidi celou firmu, vidi vsechny jeji pozadavky, ostatni jen ty svoje.

Zalozka Firma

Bylo to roztazene mezi dve mista. Nove je pod Firma vsechno, co se firmy tyka: Prehled, Resitele, Sekce, Role a prava, Typy ticketu, Pozvanky. V Nastaveni zustal Muj ucet a platformni veci, ktere klient nevidi.

Role a Typy jsou ted samostatne komponenty (RolesAdmin, TicketTypesAdmin), ne kus stranky nastaveni.

Sekce maji vlastni panel misto obecneho EntityAdmin: u kazdeho clenstvi je prepinac "vidi vse ze sekce" a radek na cloveka obecny editor s poli neumi.

Overeno na bezici instanci

Tri ucty nad tymiz daty:

Kdo Vidi
spravce platformy vsechny 3 tickety vcetne neprirazeneho
Vomacka, vede Servicedesk 2, tedy tickety obou clenu sekce
Kriz, radovy clen tehoz 1, jen svuj

K tomu: adresa cizho ticketu vraci 404, vytizeni tymu ukazuje jen viditelne, a po zaskrtnuti priznaku u clenstvi se rozsah zmeni hned.

Co z toho plyne

Neprirazeny ticket bez skupiny nevidi nikdo krome toho, kdo vidi celou firmu. Je to spravne podle modelu - takovy ticket nepatri do zadne sekce - ale znamena to, ze prichozi praci musi nekdo smerovat, jinak radovym clenum nikdy nedorazi.

2026-09-02 - Obsah ticketu se na detailu cte

body je jeden retezec, ale co v nem stoji, urcuje ten, kdo ho naplnil. Krok "Zalozit nebo doplnit ticket" dosadi {{data}} a sablona objekt prevede na JSON, takze v obsahu skoncila slozena zavorka vypsana jako odstavec. Necitelne, i kdyz jsou v ni presne ty udaje, kvuli kterym ticket vznikl.

Nova web/src/components/dashboard/TicketBody.tsx telo prectе:

Co v tele je Jak se to ukaze
veta od cloveka text tak, jak prisel
cely retezec je JSON tabulka klic a hodnota
seznam objektu tabulka se sloupci podle klicu
text a za nim JSON odstavec a pod nim tabulka
cokoliv, co se neprecte text tak, jak prisel

Ctvrta radka je bezny pripad: sablona {{voicebotId}}, {{data}} da presne tohle. Deli se az od prvni zavorky, od ktere je zbytek platny JSON - bez te druhe podminky by veta s jednou slozenou zavorkou skoncila rozpulena.

Zanoreni se kresli o uroven niz jako vnorena tabulka, hloubeji uz jako JSON: tabulka v tabulce v tabulce se necte a odesilatele umi zanorit data hluboko. Nic se nezahazuje, neprecteny obsah se ukaze jako puvodni text.

Overeno na osmi tvarech tela vcetne toho, ktery na instanci opravdu vznika, vety se slozenou zavorkou a nedopsaneho JSONu.

2026-09-02 - U posledniho volani je videt, co presne prislo

Prvni verze seznamu poslednich volani ukazovala cas a duvod odmitnuti, ale ne data. To je k nicemu: "parametr data ma mit typ text" nerekne, co tedy prislo, a bez toho zbyva hadat. Argument, ze se telo nema drzet kvuli zakaznickym datum, navic neobstal - tickety si cela prijata tela u udalosti drzi uz davno, takze si to jedno misto zakazovalo neco, co aplikace jinde bezne dela.

Radek volani se ted rozklikne na dve veci:

  • rozpad po parametrech: jmeno, cesta, co se cekalo a co na te ceste opravdu bylo, vcetne nahledu hodnoty. Sklada ho readPayload, protoze jen tam je videt kontrakt i telo naraz - pozdeji uz se kontrakt muze zmenit
  • cele prijate telo, vcetne klicu, ktere odesilatel posila navic a kontrakt o nich nevi. Delsi nez 8 kB se usekne
ok     callSid  | cekame string, povinny | prislo text = CA-B
CHYBA  status   | cekame string, povinny | prislo nepřišlo
CHYBA  data     | cekame object          | prislo text = tohle je text misto objektu

Telo se do zaznamu zmrazi na retezec, aby se pozdeji nezmenilo pod rukama, a serializace je v try - cyklicka struktura nesmi shodit prijem webhooku.

2026-09-02 - Kontrakt webhooku: objekt jde vybrat a odmitnuti je videt

Parametr spoustece data byl deklarovany jako type: string, required: true, zatimco odesilatel ho posila jako objekt a v prvni zprave hovoru ho jeste nema. Kazde volani proto skoncilo na 400 a automatizace hodinu nedelala nic.

Za tim byly tri veci, kazda sama o sobe malicherna:

  • Rucne pridany parametr byl vychozi povinny, zatimco parametr odvozeny z ukazkoveho tela nepovinny. Dve ruzna vychozi nastaveni pro tutez vec v jednom formulari. Nove je nepovinny i rucne pridany: povinny znamena "odmitni volani" a do toho nema nikdo spadnout omylem.
  • Objekt a seznam neslo vybrat. declarableFieldTypes nabizel jen string, number, boolean a date, a TriggerConfig.tsx mel jeste treti kopii toho seznamu. Deklarovat data jako objekt tedy neslo, i kdyz matchesType objekt umi a operatorsByType pro nej ma operatory. Ted jsou v nabidce oba a klient si vlastni kopii nedrzi.
  • Odmitnuti nebylo nikde videt. Skoncilo jako console.warn v logu kontejneru: zadna udalost, zadny beh, nic na detailu automatizace.

Ten treti bod je ten podstatny. Chybu v kontraktu udela ten, kdo ho psal, ale 400 dostane odesilatel - a ten s tim nic nenadela, casto je to cizi sluzba, ktera volani neopakuje. Majitel automatizace se nedozvi nic a v portalu vypada vsechno v poradku.

Detail automatizace proto ukazuje poslednich deset volani: cas, jestli proslo nebo ne, a u odmitnutych duvod. Telo se schvalne neuklada, duvod uz rika, co je spatne, a drzet payloady by znamenalo mit v pameti kopie zakaznickych dat. Seznam je v pameti, restart ho zahodi - je to diagnostika posledni hodiny, ne historie. Incident se z toho nezaklada a upozorneni se neposila: staci radek, protoze implementator se ozve sam a tady je videt co.

Vzorova automatizace ma data opravene na type: 'object', required: false.

Overeno na bezici instanci s vlastnim DATA_DIR: telo s data jako objektem projde, prvni zprava hovoru s data: null projde, telo bez callSid se dal odmita - a vsechna tri jsou videt v seznamu poslednich volani.

2026-09-02 - Navrh: pristupny portal a viditelnost

Novy 25-navrh-pristupny-portal.md. Je to navrh, ne popis stavu, nic z nej zatim neni naprogramovane.

Vzniklo to z otazky, jak portal priblizit cloveku, ktery ho nikdy nevidel. Odpoved se rozpadla na pet veci, ktere spolu souvisi vic, nez to vypada:

  • prehled ukazuje jen cisla, zadna slovesa. Vsech sest widgetu ve vychozi sade jsou statistiky a seznamy. Navrh pridava "Moje tickety", "Fronta bez resitele", "Co potrebujete udelat" a "Zaciname"
  • formulare nemaji spolecnou vrstvu. inputClass je nadefinovany na 13 mistech a rozesel se do peti ruznych vzhledu, Field je napsany trikrat. V components/ui/ neni zadny formularovy prvek
  • hledani neumi to jedine, k cemu je. Klientsky filtr nehleda v obsahu, ve vlastnich polich ani v externim ID, takze hovor podle callSid se dohledat neda. Navrh je modal s kriterii, protoze ticket nema pevnou sadu poli
  • viditelnost ticketu se neda omezit. Pohled tenant dostane kazdy, kdo do firmy patri, mine je dobrovolny filtr a ne strop. Navrh vede viditelnost pres clenstvi (priznak na firme, priznak u kazde skupiny), ne pres role - role jsou na celou firmu a neumi rict "v jedne sekci vidim vse, v druhe svoje"
  • uloziste neprezije nasazeni. Overeno v praxi: po dnesnim nasazeni zustaly ve firme dva tickety, predtim jich byly tisice

Dve veci, ktere stoji za zapamatovani, i kdyby se navrh nikdy nedodelal:

  1. Strop viditelnosti nepatri do hledani, ale do cteni ticketu. Cesty k ticketum jsou dnes tri (listTickets, getWorkload, getAgentStats) a dve z nich filtruji tickets primo. Kdyby strop resilo jen hledani, obejde se widgetem nebo souhrnem.
  2. Pohled a strop nejsou totez. Pohled je co chci videt, strop je co vubec smim videt. Dnes existuje jen pohled a klientovi se veri.

Ctyri otevrene otazky jsou v zaveru navrhu: co znamena "moje" pro strop, jak se strop potka s helpdeskem, jestli hledat i v udalostech a kdo smi viditelnost nastavovat.

2026-09-02 - Vzorova automatizace srovnana s bezici instanci

seedRealAutomations v src/data/automationStore.ts drzelo starsi podobu stromu nez ta, ktera na instanci opravdu bezi. Protoze data neprezivaji redeploy, je tenhle seed jedine misto, kde nastaveni prezije nasazeni - a kdyz se rozejde, znamena to po kazdem nasazeni stavet strom rucne znovu.

Opsano z bezici instance: spoustec ma sest parametru misto tri (pribylo result, rating a data, vsechny s cestou do data), krok "Zalozit nebo doplnit ticket" pise do obsahu {{voicebotId}}, {{data}} a stav bere z {{result}}, a za nim je podminka nad vysledkem: cokoliv krome "Chybějící informace" ticket zavre, jinak jde na servicedesk s vysokou prioritou.

Pravidlo, ktere z toho plyne a je i v komentari u funkce: kdyz se strom na instanci zmeni, patri ta zmena sem. Jinak ji dalsi nasazeni zahodi.

2026-09-02 - Zalozit NEBO DOPLNIT ticket: doplneni konecne doplnuje

Automatizace mela v kroku "Zalozit nebo doplnit ticket" pole Obsah nastavene na {{rating}}. Data v behu prokazatelne byla - v udalostech ticketu je hodnoceni videt cele - ale ticket zustal s prazdnym obsahem.

Cim to bylo

intakeEvent deli praci na zalozeni a navazani na existujici ticket. Vsechno z create platilo jen pro tu prvni vetev. U existujiciho ticketu se doplnovaly pouze vlastni pole a stitky. Predmet, obsah, typ, priorita, kanal, zakaznik, resitel ani skupina ne - tise se zahodily.

U hovoru to znamena, ze obsah nedorazi nikdy. Prvni zprava jen oznami, ze hovor zacal (status: in-progress, data: null), a prave ta ticket zaklada, tedy s prazdnym obsahem. Hodnoceni prijde az posledni zpravou, kdy uz ticket existuje.

Stav byl jedina vyjimka, protoze ho krok nastavuje zvlast pres updateTicketStatus. Proto fungoval a zbytek ne, a proto to vypadalo jako chyba jednoho pole.

Jedno pravidlo misto dvou seznamu

Zalozeni a doplneni ted delaji totez:

Neprazdna hodnota prepise, prazdna nemaze.

IntakeInput ma na to apply, v create zustal jen zaloha predmetu a vychozi stav. Dva ruzne seznamy poli by se stejne zase rozesly a nekde by zas neco chybelo.

Prazdna hodnota nemaze schvalne. Prave to byla puvodni obava, kvuli ktere se zapisovalo jen pri zalozeni: pozdejsi zprava bez jmena zakaznika je bezna a smazat kvuli ni jmeno by bylo horsi nez ho nedoplnit. Nove se prepise jen to, co odesilatel opravdu poslal, takze pojistka plati a data se neztraci.

Dve vyjimky zustavaji: zaloha predmetu z externiho ID plati jen pri vzniku a stav chodi pres updateTicketStatus, ktere resi i priznak vyrizeni, cas vyreseni a pocet znovuotevreni.

Data smi chodit po castech

Vlastni pole typu se scitaji podle klicu. Prvni zprava posle data, druha data2 a ticket ma obe. Odesilatel se nemusi predem dohodnout, co vsechno posle, a nemusi posilat vsechno pokazde.

Prazdny retezec pritom pole nemaze. Sablona, ktera na nic neukazuje, se dosadi prazdnem, takze {"vysledek":"{{result}}"} u zpravy bez vysledku posilalo prazdno a prepsalo tim hodnotu z minule zpravy. Vymazat pole jde poslanim null: to uz je zamer, ne vedlejsi ucinek nevyplnene sablony.

Stav se ted ulozi uz pri vzniku

create.status se do createTicket vubec nepredaval, takze ticket vznikl s vychozim "Nový" a hned se prepsal. V logu pak stalo "stav Nový -> completed" u ticketu, ktery v "Nový" nikdy nebyl. Komentar u volajiciho tvrdil, ze uz je to opravene - opravena byla jen jedna strana.

Faze dorazena do konce

Faze byla zrusena uz driv, hodnoty z ni patri do stavu. V katalogu po ni ale zbyval krok "Posunout do dalsi faze" a pole Faze u zalozeni ticketu. Ani jedno nemelo co delat: ticket ani typ ticketu fazi nemaji. Krok by pri behu selhal na chybejicim skriptu a pole se tise zahazovalo, takze {{status}} napsany do Faze nedelal nic. Oboji je pryc.

Aby bylo videt, ze se to ulozilo

Krok v logu rekne, co doplnil: doplnen TK-123, stav completed, obsah. Driv radek jen oznamil, ze se ticket doplnil, a nebylo poznat cim - u dat, ktera dorazi az druhou zpravou, je to zrovna ta informace, kterou clovek hleda.

2026-08-28 - pad portalu uz nesmi shodit stranku a zaklada incident

Ukazka tela webhooku shazovala cely builder pri psani cesty parametru. Chyba sama byla na jednom radku, ale to podstatne je, ze jedna vyjimka pri vykreslovani odstranila celou stranku. Uzivateli zustala bila plocha a rozdelana prace byla pryc. Aplikace na to nemela zadnou pojistku.

Pojistka proti padu vykreslovani

Nova web/src/components/ErrorBoundary.tsx obaluje obsah portalu. Mistni chyba ted shodi svoji cast obrazovky, ne aplikaci: navigace, prepinac firmy i odhlaseni zustanou funkcni a uzivatel ma kam odejit. Odchod na jinou stranku pojistku srovna zpatky.

Je to jedina trida v celem klientovi. Hook na tohle neni a nebude, React to umi zachytit jen takhle.

Pad zaklada incident

POST /api/dashboard/client-crash. Bez toho je jedina stopa v konzoli prohlizece uzivatele, kam se nikdo nedostane - takze bychom o padu vedeli jen tehdy, kdyby ho nekdo nahlasil. To znamena o vetsine padu nevedet.

Incident nese dve casti, stejne jako ostatni incidenty:

  • title a impact cte zakaznik, tedy zadne stack trace,
  • detail cte spravce platformy: hlaska, misto v kodu, strom komponent, adresa stranky, ucet, prohlizec a verze buildu.

Verze buildu je tam schvalne. U tohohle padu se ukazalo, ze bez ni se neda poznat, jestli uzivatel vidi chybu, ktera uz je opravena, nebo novou.

Tentyz pad na tomtez miste zalozi incident nejvys jednou za deset minut. Pad pri vykreslovani se opakuje pri kazdem prekresleni a jinak by z jedne chyby vzniklo padesat incidentu a ten pravy by v nich zapadl.

Sama ukazka

  • Do cesty parametru se da zanorit vzdy. Kdyz na miste je skalar, nahradi se schrankou - driv se zapisovalo do retezce a to shodilo stranku.
  • Parametr, kterym vede cesta jineho parametru, uz nedostane zastupnou hodnotu podle typu. Plati vnitrni struktura: kdyz je jeden parametr data a druhy data.result, data musi byt objekt. Zastupna hodnota by ho prepsala.
  • Cela ukazka je navic v try. Je to napoveda a nesmi shodit ani ten kus obrazovky, at uz do ni prijde cokoliv - parametry se pisou znak po znaku a rozdelany stav je normalni.

2026-08-28 - MCP EasyWeb podle skutecne specifikace (auth v2)

Predchozi verze posilala na /login jen jmeno, heslo a nazev zarizeni. Server na to odpovidal 400 Bad Request na cokoliv, i na spravne udaje, protoze cekal neco uplne jineho.

EasyWeb ma auth v2: token se nevydava proti uctu, ale proti zarizeni, a to je pár klicu ECDSA P-256. Jmeno a heslo se pouziji jedinkrat, kdyz se klic registruje, a soucasti registrace je podpis, kterym zarizeni dokazuje, ze privatni klic k poslanemu verejnemu opravdu ma. Od te chvile se podepisuje kazde volani, ktere s tokeny hybe.

Overeno proti bezicimu serveru: se spravnym telem uz /login nevraci 400, ale 401 s neplatnymi udaji. Ucty z jejich testovaciho settings.json na verejnych instancich neplati, takze dal se bez skutecnych udaju nedostanu.

Prihlaseni

  • src/mcp/easyweb/crypto.ts - klice, podpisy, otisky. Podpis musi byt P1363, tedy holé r || s, 64 bajtu. Node podepisuje ve vychozim nastaveni do DER a ten by protistrana neuznala.
  • src/mcp/easyweb/device.ts - klic zarizeni se vyrobi jednou a prezije restart: uklada se mezi udaje konektoru, ktere uz jsou zasifrovane. Pole ma novy priznak managed, takze ho ve formulari nikdo nevidi.
  • src/mcp/easyweb/session.ts - tri tokeny, retez s ustupy (platny pristupovy, obnova obnovovacim, obnova zarizenim, cele prihlaseni), jedno prihlaseni naraz na konektor, tokeny jen v pameti.

Ta posledni tri pravidla nejsou opatrnost navic: tokeny jsou jednorazove, druhe pouziti server odmita kodem 409 a umi zarizeni zablokovat. Dve soubezne automatizace nad tymz napojenim by bez jedne sdilene rozdelane operace spustily dve obnovy a druha by pracovala se spotrebovanym tokenem.

Transport

  • Server si sam vybira, jestli odpovi JSON telem, nebo SSE streamem, a streamem odpovida i na obycejna volani. Klient proto nabizi obojí a cte stream po kouscich - u dlouhych uloh ho server sam nezavira a posila do nej tlukot srdce, takze cekani na konec by skoncilo az timeoutem.
  • Handshake plati na token, ne na volani. Server drzi sezeni u tokenu.
  • Odmitnute sezeni prijde jako chyba -32008 uvnitr uspesne odpovedi. Bez zvlastniho osetreni by to vypadalo jako chyba volani a krok by skoncil misto toho, aby se prihlasil znovu.
  • Seznamy se skladaji pres vsechny stranky. Bez toho je videt jen prvni, tedy asi dvacet nastroju, a vypada to jako uplny seznam.
  • Odpoved se rozbaluje rekurzivne (structuredContent, contents, content, JSON zapsany jako text). Jinak by v kroku skoncil JSON jako retezec, se kterym uz se v podmince nic nesvede.

Dlouho bezici nastroje

Nastroj, ktery server odmitne spustit rovnou, se spusti jako uloha a ceka se na ni dotazovanim. Stav z tasks/list a tasks/get je to jedine, co je vzdycky pravda - notifikace o prubehu chodi nejvys jednou. Dokoncena uloha ze seznamu mizi, takze po nekolika marnych kolech se prejde na primy dotaz.

Limit kroku se pri tom posouva z patnacti sekund na deset minut. Kdyz uloha nedobehne ani tak, neni to chyba: na serveru bezi dal a krok to rekne.

Co z toho zbylo nedodelane

Trvaly kanal notifikaci (GET SSE), nahravani souboru po castech a hlidani zmen kontraktu podle verze serveru. Vsechno je v 24-mcp-konektory.md v seznamu toho, co chybi.

2026-08-28 - dve MCP sluzby: obecna a EasyWeb, strankovani nastroju

MCP je standard, ale prihlaseni k nemu ne. Oficialni specifikace stoji na OAuth 2.1 a objevovani autorizacniho serveru pres .well-known. EasyWeb (Centaur) ma prihlaseni vlastni: POST /login s HTTP Basic vrati trojici tokenu a obnovuje se vlastnimi endpointy. Zadny OAuth, zadne .well-known, jina verze protokolu, zadne SSE ani hlavicka sezeni.

Proto jsou v katalogu dve sluzby, ne jedna s prepinacem: firma pri zakladani konektoru vyplnuje neco jineho. U obecne ID a tajemstvi aplikace nebo hotovy token, u EasyWebu jmeno, heslo a nazev zarizeni. Slucovat to by znamenalo formular, kde je pulka poli vzdycky k nicemu, a hadani, ktera pulka to je.

Obecna sluzba pritom zustava plnohodnotna. Vlastni server je duvod pridat sluzbu, ne duvod zavrit dvere ostatnim.

Pribylo

  • src/mcp/dialect.ts - rozdily obou serveru na jednom miste: prihlaseni, verze protokolu, jestli se prijima SSE a jestli se posila Mcp-Session-Id. Rozesete po klientovi by u kazdeho dalsiho serveru pribyl dalsi if jinde.
  • Sluzba MCP EasyWeb. Adresa, jmeno, heslo, nazev a otisk zarizeni. Prihlasovaci adresy si portal odvodi z adresy serveru sam. Otisk se doplnuje z ID konektoru, aby server poznal totez zarizeni.
  • Hotovy token u obecne sluzby. Rada verejnych serveru nic jineho nenabizi a bez toho by na ne neslo zalozit konektor.
  • Objevovani pres WWW-Authenticate. Specifikace to ma jako povinnou cestu: server u odpovedi 401 rekne, kde jsou jeho metadata. Pouziva se az kdyz obvykla mista selzou, protoze to stoji volani navic.
  • Zivotnost z tela tokenu. Kdyz server expires_in ani datum neposle, cte se exp z JWT. To je presne pripad EasyWebu.
  • Strankovani nastroju. Nastroj s parametrem cursor dostane v builderu prepinac Nacist vsechny stranky. Kurzor je hodnota z odpovedi, takze v dobe stavby stromu ho nikdo nezna a nejde ho vyplnit dopredu. Krok pak vraci navic items, pages, pageCount a truncated. Strop je 20 stranek.

Opraveno

Prihlaseni driv zkousela password grant a HTTP Basic proti hlavnimu endpointu. Prvni OAuth 2.1 zrusil, druhe neni nikde ve specifikaci a u EasyWebu by stejne neproslo - ten chce Basic na /login, ne na /mcp.

2026-08-28 - MCP: prihlaseni jmenem a heslem, tokeny si resi portal

Predchozi verze chtela po uzivateli token. To bylo spatne zadani: zakaznik dostane ke svemu serveru adresu, jmeno a heslo, token nedostane a nema jak ho ziskat - vyda ho az autorizacni server a ma omezenou zivotnost.

Novy src/mcp/auth.ts resi cely zivotni cyklus:

  • Kde se prihlasit se zjisti od serveru pres /.well-known/oauth-protected-resource a metadata autorizacniho serveru. Rucni pole na adresu je jen zaloha pro servery, ktere metadata nemaji.
  • Cim se zkusi v poradi client_credentials a password. Ktere z toho firma dostala, se z udaju samych poznat neda a nutit ji to vybirat by znamenalo ptat se na neco, co nevi.
  • Bez autorizacniho serveru se posle HTTP Basic. Mensi servery zadny OAuth nemaji a jmeno s heslem je u nich presne tohle.
  • Zivotnost urcuje server. Token se vymeni minutu pred vyprsenim, obnovi se pres refresh_token, kdyz ho server vydal, a kdyz expires_in chybi, pocita se s peti minutami - tedy odhaduje se dolu.
  • Odmitnuty token (401 na token, ktery jsme meli za platny) se zahodi a volani se zopakuje jednou. Podruhe uz ne, to uz nejsou platne udaje.

Token se drzi jen v pameti. Je kratkodoby, po restartu se o novy rekne znovu, a ulozit ho by znamenalo vsechna rizika ulozeni bez jakekoliv vyhody. Kes si drzi otisk udaju, takze zmena hesla ulozeny token zneplatni.

Zpusob prihlaseni je videt v hlasce u konektoru: uzivatel vyplnil jmeno a heslo a ma vedet, jak s nimi portal nalozil, nez zacne hledat chybu jinde.

2026-08-28 - MCP konektory hotove

Firma si zalozi napojeni na svuj MCP server, stiskne Nacist nastroje a jeho nastroje se objevi v builderu jako kroky. Popis v 24-mcp-konektory.md.

Navrh byl predtim dvakrat prepsany a stoji za to rict proc. Prvni verze davala nastroje do vyberu kroku, ale popisovala MCP jako obycejny katalog vzdalenych procedur - tak nedava nic navic proti HTTP konektoru. Druha verze to prehnala opacnym smerem a chtela nastroje predavat jen modelu. Vysledne zadani je uprostred: klientem jsme my, nastroj vybira clovek v builderu, a prinos je v tom, ze napojeni na cizi system vznikne bez radky kodu u nas.

Pribylo

  • Sluzba mcp. Jedina v katalogu, ktera nema zadne pevne operace - rekne je az server. Udaje jsou adresa serveru, token nebo klic v X-API-Key.
  • POST /connectors/:id/mcp/tools. Zepta se serveru na tools/list a ulozi vysledek. Je to zaroven overeni konektoru, proto tlacitko "Overit" u MCP neni.
  • Klient protokolu (src/mcp/client.ts): handshake, sezeni z hlavicky odpovedi, odpoved jako JSON i jako SSE stream, strankovani nastroju.
  • Prevod schemat (src/mcp/schema.ts). Ze schematu vzniknou pole kroku a zpatky se z vyplnenych retezcu udelaji argumenty ve spravnych typech. Ten druhy smer je ten podstatny: server ceka {"limit": 10}, ne {"limit": "10"}, a rada serveru na tom spadne az uvnitr nastroje.
  • Nastroje u konektoru (sloupec mcp, migrace 004). Bez ulozeni by po restartu zmizely z katalogu kroky, ktere uzivatel uz ma ve stromech.
  • Vnitrni krok runMcpTool. Jedna obsluha pro vsechny nastroje vsech serveru - ktery to je, rika az ID operace tool:<konektor>:<nastroj>.

Rozhodnuti, ktera stoji za zminku

  • Nastroj patri firme, ne katalogu. serviceCatalog(tenantId) bez firmy nevrati zadny nastroj. Zapomenuty argument tak znamena "nic", ne "vsechno" - stejne pravidlo jako u filtru na firmu.
  • ID operace nese ID konektoru. Firma muze mit dva servery a na obou nastroj search.
  • Krok se neopakuje. MCP nema idempotencni klic, druhy pokus po timeoutu by nastroj provedl podruhe.
  • Chyba nemaze nastroje. Vypadek serveru nesmi vymazat kroky z hotovych automatizaci. Prazdny seznam se naopak ulozi.
  • Servery se pri startu neobvolavaji. Jeden nedostupny by shodil katalog vsem.

Opraveno mimochodem

  • setStatus v connectors/postgres.ts melo v RETURNING doslovny retezec ${COLUMNS} misto dosazeni. V rezimu file, ve kterem bezi nasazeni, se to neprojevilo.
  • Dva odstavce v 12-sluzby-a-konektory.md byly dvakrat.

2026-08-28 - dokument pro programatory

Novy 00-pro-programatory.md: rozcestnik, model ctyr pojmu (sluzba, skript, konektor, krok), pravidla, ktera plati vsude, tri cesty vzniku operace, tri rezimy uloziste a seznam mist, ktera jsou krehka.

Neni to kopie 03-architektura-a-mapa-kodu.md. Ta rika, kde co lezi, tenhle rika proc to tak je.

2026-08-28 - transformace maji svou kategorii, pribylo XML

Odstraneno

  • Sluzba AI zpracovani textu. Delala totez co OpenAI, jen jinymi slovy. Dva zpusoby, jak udelat jednu vec, znamenaji jen otazku, ktery pouzit. Ukazkove stromy a stopy, ktere na ni odkazovaly, ted miri na openai.chat.

Pridano

  • Kategorie Transformace dat. Driv byly mezi obecnymi sluzbami vedle webhooku a pauzy. Ve chvili, kdy jich je vic nez tri, je clovek hleda jako skupinu, ne mezi spoustecemi.

  • Skript transform.to-xml. Zapise objekt jako XML vcetne escapovani. Cizi systemy si XML porad rikaji casto (ISDOC, EDI, banky, statni sprava) a bez tohohle se sestavuje retezcem v HTTP kroku, kde se na escapovani zapomene pokazde.

    Klice, ktere nejsou platny nazev prvku, se prejmenuji a rekne se to nahlas. Klic zacinajici cislici dostane podtrzitko misto odriznuti - 2024 a 2025 by jinak byly totez a v dokumentu by se srazily.

  • Navrh cele skupiny transformaci v 13-transformace-dat.md: formaty, pole a hodnoty, seznamy, text a kontrola. Vcetne toho, co v seznamu zamerne neni a proc.

2026-08-28 - AI zpracovani textu funguje

Sluzba byla v katalogu bez pristupovych udaju a s appId: 'ai-text', tedy mirila na aplikaci, ktera neexistuje. Konektor nemel co vyplnit a krok nemel co vykonat.

Zmeneno

  • Sluzba ma API klic a skutecnou adresu. Stoji na api.openai.com/v1 stejne jako OpenAI, overeni je GET /models. Model se bere z konektoru (ctx.config.model), aby se nevyplnoval u kazdeho kroku znovu.

Pridano

  • Tri skripty: ai-text.classify, ai-text.extract, ai-text.generate.

Proc to neni jedna sluzba s OpenAI: tam se posila volny dotaz a vraci volny text. Tady je uloha pevna a vystup taky - category a confidence, objekt s popsanymi poli, text s poctem slov. Prave na to jde ve strome navazat podminkou, na volny text ne.

Zjisteno u toho

  • Overeni konektoru projde i u sluzby, ktera neexistuje. Proxy vraci na neznamou aplikaci 200 s prazdnym telem, a sluzba bez verifyPath se overuje prave pres /health. Konektor tedy rekne "Sluzba odpovida" i tam, kde nic nebezi. Tyka se to E-shopu, WhatsAppu, Facebooku, Instagramu, SMS a Slacku. Zapsane v 21-realne-sluzby.md, zatim neopravene.

2026-08-26 - jazyky, skutecny snimek v heru a opravene titulky

Pridano

  • Prepinani jazyku (web/src/i18n/). Vybrany jazyk je jedna hodnota pro celou aplikaci, drzena mimo React - tentyz vzorec jako u vybrane firmy. Volba prezije obnoveni stranky a prepisuje <html lang>. Anglicky slovnik je Partial: co v nem neni, spadne na cestinu, takze preklad jde doplnovat po castech misto jednoho velkeho skoku. Podrobnosti v 23-jazyky.md.
  • Prepinac jazyka v hlavicce a v mobilnim menu.

Zmeneno

  • Snimek v heru vypada jako portal doopravdy. Byl svetly a ukazoval obrazovku, ktera v produktu neexistuje. Ted ma tmavy podklad, tytez zalozky vcetne prepinace firmy, tytez sloupce tabulky i tytez odznaky stavu jako stranka Tickety. Kdyz se navstevnik prihlasi a uvidi neco jineho, prvni dojem z produktu je, ze web lhal.
  • Jmeno znacky si sklada usePageMeta, ne stranky. Bylo napsane natvrdo ve dvaceti ctyrech titulcich, takze prejmenovani produktu zmenilo brand.ts a titulky ne - hlavicka rikala nove jmeno a zalozka v prohlizeci stare.

Opraveno

  • Odkaz "Prohlednout si to" vedl mimo proxy. Byl to <a href="/sluzby">, tedy koren domeny, ne /apps/<app-id>/sluzby. Ted je to Link z routeru jako vsude jinde. Byl to jediny takovy odkaz v celem webu.

2026-08-26 - na aplikaci je videt, ktery build to je

Nasazena aplikace o dva commity pozadu funguje a vypada skoro stejne jako nova. Bez oznaceni buildu se to nepozna a hleda se chyba tam, kde zadna neni.

Pridano

  • Oznaceni buildu v pate postranniho menu, pod tlacitkem Odhlasit se: verze, cas buildu a cislo commitu. Text jde oznacit jednim kliknutim, aby se dal poslat dal.
  • Totez jako <meta name="app-build"> v hlavicce stranky. V JS balicku uz to je, ale ten se musi stahnout a rozbalit. Meta znacka je videt na jeden curl bez prihlaseni - a prave to clovek potrebuje, kdyz zjistuje, co bezi na produkci.
  • Hodnoty dosazuje Vite pri buildu (define a plugin build-stamp ve vite.config.ts), takze neplati pro repozitar, ale pro ten konkretni balicek.

Zmeneno

  • .dockerignore uz nevylucuje .git/. Build z nej cte cislo commitu; bez nej by se poznal jen cas buildu, ne co v nem je. Do vysledneho image se .git nedostane, ten se sklada z ciste zakladni image.

2026-08-26 - rebrand na WorkNuke

Prevzeti vizualniho smeru z predlohy. Popis, jak znacka funguje v kodu, je v 22-znacka-a-design.md.

Zmeneno

  • Barevnost. Akcent z cyanove na zelenou #B7FF3C, podklad z modrocerne na zelenocernou #0F1411. Web i portal stoji na tychz tokenech, takze se to projevilo naraz na obojim.
  • Druhy akcent uz neni barva. accent-* ukazuje do teze zelene rady jako brand-*. Klice zustaly, protoze je pouziva 341 mist; prepsat je vsechny naraz by byla zbytecne velka zmena. Totez u utility text-gradient, ktera uz negeneruje prechod, ale plnou zelenou.
  • Stav "v poradku" je modrozeleny. Akcent je zeleny, a dve zelene na jedne obrazovce, kazda o necem jinem, nikdo nerozlisi.
  • Pismo. Archivo na nadpisy, IBM Plex Sans na text, IBM Plex Mono na cisla a ID. Nadpisy maji tesny proklad, ktery Inter neumel bez trikareni.
  • Znak. Plocha bomba misto pismene v gradientnim ctverci. LogoMark je samostatny export, aby sel pouzit i uvnitr snimku v heru.
  • Tlacitko. Plna zelena misto prechodu ze dvou barev. Prave tim vystupuje z rady.
  • Hero. Prokladany nadtitulek misto odznaku, dvouslovne radky nadpisu, jedno primarni tlacitko plus textovy odkaz, ctyri slovesa misto tri odrazek a snimek portalu vybihajici z prave hrany.
  • Navigace je pri levem okraji a bez pilulky za aktivni polozkou. Jedina pilulka na liste je primarni akce.
  • Nazev, claim a kontakt v brand.ts.

Opraveno u toho

  • Zare v heru je pozadi sekce, ne vrstva pod ni. Prekryv se zapornym z-indexem se schoval za podklad stranky a nebyl videt vubec.

Co se zamerne nezmenilo

Klice v prohlizeci (automia.token, automia.tenant), ID firmy tnt_automia, ID aplikace csbot-prototype a prihlasovaci e-maily v ukazkovych datech. Vypadaji jako jmeno, ale jsou to identifikatory. Duvody jsou v 22-znacka-a-design.md.

2026-08-26 - prepinac firmy: do sidebaru a prekresli obsah cely

Dodelavka predchoziho bodu. Tvrzeni "prepinac plati pro cely system" nebylo pravdive: useApiQuery se na zmenu firmy zeptat umel, ale komponenty, ktere volaji apiFetch primo - EntityAdmin, a tim cele Nastaveni a zalozky Resitele a Skupiny v Lidech - o zmene nevedely a zustala na nich data predchozi firmy.

Opraveno

  • 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.

Zmeneno

  • Prepinac je v sidebaru nad navigaci, ne v hlavicce. Vypada jako ovladaci prvek: ram, ikona firmy, sipka. Predchozi verze mela pruhledny ram a splyvala s popiskem - prepinac, ktery neni videt, je k nicemu.
  • V hlavicce zustal jen nazev aktivni firmy. Driv tam stalo natvrdo tenants[0], takze po prepnuti ukazovala porad prvni firmu ze seznamu.
  • Kdo patri do jedne firmy, vidi misto prepinace jeji nazev. Vyber z jedne moznosti neni vyber.

2026-08-26 - prava plati za firmu, ne za cloveka

permissionsOf(user) scitalo role pres vsechna clenstvi. Kdo byl spravce v jedne firme, jednal jako spravce ve vsech, kam patril. Uzivatel ve dvou firmach je vzacny, dusledek ne - to neni edge case, to je diera v pravech.

Zmeneno

  • 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. Kdo edituje zaznam jine firmy, musi mit pravo tam, ne tam, kde je zrovna prepnuty.
  • 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. Bez toho by prvni dotaz odpovedel i na druhou.
  • readScope cte z jedne firmy, te prepnute, ne ze vsech, kam clovek patri. V nastaveni se driv michaly typy ticketu a role napric firmami bez ohledu na vyber - stejna vada jako u prepinace, jen na jinem miste.

Opraveno

  • 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 a obe prava helpdesku by spravci firmy nikdy nenabehla a zalozka by se neobjevila. Meni se jen prava, jen u nasich roli, nazvu se to netyka.

2026-08-26 - prepinac firmy plati pro cely portal

Prepinac byl stav uvnitr stranky Prehled. Prepnuti na LogiTrans zmenilo prehled a nic jineho: ostatni stranky volaly API bez tenantId a server sahnul po vychozi firme, takze v Lidech zustali lide Automie. To neni nepohodli, to jsou cizi data pod hlavickou jine firmy.

Zmeneno

  • Vybrana firma je jedna hodnota pro cely dashboard (web/src/lib/tenant.ts), ne stav stranky. Prezije obnoveni stranky.
  • tenantId doplnuje apiFetch, ne jednotlive stranky. Kdyz si to mela pridavat kazda stranka sama, vetsina na to zapomnela - a zapomnetlivost byla prave ta chyba. Nedoplnuje se, kdyz cesta uz firmu nese (proklik z widgetu), u scope=all a mimo /api/dashboard.
  • useApiQuery se pri prepnuti zepta znovu. Bez toho by na strance zustala cisla predchozi firmy, dokud by ji nekdo neobnovil.
  • Prepinac je v hlavicce nad obsahem. Driv byl v Prehledu, ted plati viditelne pro vsechno. Nahradil i text, ktery ukazoval natvrdo prvni firmu ze seznamu bez ohledu na to, co bylo vybrane.
  • Ulozena firma, do ktere uzivatel uz nepatri, spadne na vychozi. Jinak by po odchodu z firmy koncil kazdy dotaz na 403.

Zjisteno u toho

  • Prava se scitaji pres vsechny firmy, neprepocitavaji se podle vybrane. Kdo je spravce v jedne firme a resitel v druhe, ma po prepnuti porad prava spravce. Data se tim neprolomi, server je filtruje dal podle firmy, ale tlacitka ukazuji vic, nez by mela. Zapsane v 07-firmy-a-prava.md, sekce Co chybi.

2026-08-26 - helpdesk: pozadavek, ktery vidi zadavatel i resitel

Zakaznik nemel jak poslat pozadavek. Ticket pritom patri jedne firme, kdezto u helpdesku figuruji dve - ta, ktera se pta, a ta, ktera to resi.

Pridano

  • Pole Ticket.helpdeskSourceId s firmou, ktera pozadavek poslala. Vlastnikem (tenantId) zustava ta, ktera ho resi. Zamerne tak: kdyby byl vlastnikem zadavatel, mel by resitel pozadavek jen jako cizi ticket a nemel by ho ve sve fronte, ve statistikach ani v prirazovani. Takhle je to na jeho strane obycejny ticket a nemuselo se sahnout na nic, co uz funguje.
  • Tenant.helpdeskProviderId, tedy komu firma posila pozadavky. Nastavuje spravce platformy v Nastaveni, Firmy. Kdo koho obsluhuje je obchodni vztah, ne volba klienta - kdyby si dodavatele vybiral uzivatel, poslal by pozadavek nekomu, s kym nema smlouvu. Bez vyplneneho dodavatele se pozadavek nezalozi a rekne se to nahlas.
  • Sekce Helpdesk (/dashboard/helpdesk) a router /api/dashboard/helpdesk: seznam vlastnich pozadavku, zalozeni, detail s prubehem a komentar. Zamerne to neni druhy seznam ticketu - chybi tu filtry, prirazovani i fronta, protoze zadavatele nezajima, kdo to ma u sebe.
  • Prava helpdesk.view a helpdesk.create. Prideluje je admin te firmy pres role, stejne jako u ostatnich prav.

Zmeneno

  • listTickets umi filtrovat podle zdroje (helpdeskSourceIds). Vyplnene nahrazuje filtr podle vlastnika, protoze zadavatel vlastnikem neni. Bezny seznam ticketu se nezmenil.
  • getTicket a addComment maji druhou cestu dovnitr. Firma, ktera pozadavek poslala, ho smi cist a pripsat k nemu komentar, i kdyz ho nevlastni. Komentar je jedina zmena, kterou nad nim smi: stav, resitele a typ urcuje ten, kdo to resi.

2026-08-26 - portal prestal ukazovat provozni veci a ticket jde zalozit rucne

Sada oprav podle toho, co v portalu drhlo.

Zmeneno

  • Hlasky o ulozisti a o odchozi IP adrese vidi jen spravce platformy. Kam se uklada a z jake adresy volame ven resime my, ne zakaznik. "Data se ukladaji do souboru na serveru" na nej navic pusobi jako priznani, ze mu tu praci muzeme ztratit. Nemazou se, jen se schovavaji - nam poradi porad. Totez plati pro radek "volano z IP" v historii overeni.
  • Typ ticketu se v automatizaci vybira ze seznamu. Bylo to textove pole, do ktereho mel clovek opsat ID typu odjinud. Novy druh pole lookup je ciselnik a volny text zaroven: bud se vybere ze seznamu typu te firmy, nebo se hodnota dosadi z dat ({{data.typ}}). Typy ticketu jsou vlastnost firmy, takze nabidka chodi za tu, ve ktere clovek je.
  • Stav ticketu je otevreny naseptavac, ne ciselnik. Stav je volny retezec a vzdycky byl - ticket muze prijit z cizi aplikace s jejim vlastnim stavem. Vyber ze seznamu tomu odporoval. Ted je to textove pole s nabidkou toho, co firma uz pouziva (GET /api/dashboard/tickets/statuses), a nova hodnota projde stejne dobre. Zapisuje se az pri opusteni pole, ne po kazdem pismenu.
  • "Kanal" se prejmenoval na "Odkud pozadavek prisel" a rika o sobe, ze je nepovinny a slouzi jen k filtrovani a ikone v seznamu.
  • Vstupni parametry u webhooku jsou oznacene jako nepovinne. Webhook prijme cokoliv; rucne vypsany seznam parametru neni podminka, ale pohodli pro podminky - a vyplni se sam z vlepene ukazky tela. Prazdny seznam uz nehlasi, ze "bez nich nelze pridat podminku".

Pridano

  • Zalozeni ticketu rucne (POST /api/dashboard/tickets, pravo ticket.create, tlacitko Novy ticket na strance Tickety). Dosud ticket vznikal jen z automatizace nebo z prichozi udalosti, takze pozadavek prijaty telefonem nemel jak do systemu.
  • Zakaznik u ticketu je nepovinny. Rucne zalozeny ticket je casto ukol, ne pozadavek od nekoho zvenku; povinna firma a kontakt by znamenaly, ze si je clovek vymysli. Ve formulari je zakaznik schovany pod odkazem.

Co z teze davky jeste neni

  • Sekce Helpdesk pro zakaznika vcetne sdileni ticketu mezi admin firmou a tim, kdo ho zalozil.
  • Prepinac firmy nad celym dashboardem. Dnes je to stav uvnitr stranky Prehled, takze se prepnuti neprojevi v Lidech ani jinde - ostatni stranky volaji API bez tenantId a server pouzije vychozi firmu. Ma to doplnovat API vrstva na jednom miste, ne kazda stranka zvlast.

2026-08-26 - odesilani e-mailu ze schranky firmy

Sluzba E-mail byla v katalogu jako popis: bez udaju, bez vykonne casti. Ted se odesila doopravdy, ze schranky, kterou si firma vyplni v konektoru.

Pridano

  • Sluzba E-mail pres SMTP. V konektoru server, port, sifrovani, uzivatel, heslo, adresa a jmeno odesilatele a adresa pro odpovedi. Zadne udaje v prostredi - kazda firma odesila ze sve schranky.
  • Vnitrni krok email/send. SMTP neni HTTP a skript umi jen ctx.http, takze operaci vykonava vnitrni krok stejne jako zalozeni ticketu. Pristupove udaje pritom zustavaji v konektoru.
  • Druh pole html. Telo zpravy se pise jako HTML a builder ho vykresli jako vysoke pole s neproporcionalnim pismem. Runtime v nem escapuje dosazene hodnoty: znacky autora sablony jsou zamer, ostre zavorky v hodnote od zakaznika ne. Bez toho by text ticketu s <b> prepsal rozvrzeni zpravy a <script> by dosel prijemci do schranky.
  • Overeni konektoru prihlasenim. Sluzba s transport: 'smtp' se neoveruje ctecim volanim, ale prihlasenim na server. Nic se neodesila, takze test nikomu nic nedoruci, a bez platneho hesla neprojde.
  • Vnitrni krok smi rict "zkus to znovu" (StepOutcome.retryable). Dosud se opakovani u vnitrnich kroku vzdy vypinalo, protoze selhavaly na spatnem nastaveni. Nedostupny posmovni server ale za minutu bezet muze.
  • Textova verze zpravy. Bez vyplneni se vyrobi z HTML. Klient, ktery HTML nezobrazi, by jinak dostal prazdnou zpravu a filtry nevyzadane posty berou chybejici textovou cast jako priznak spamu.
  • Zavislost nodemailer. Vlastnorucne psany SMTP klient by byl 300 radku, ktere nejde bez schranky overit.

Opraveno

  • SMTP server z konektoru nesmi mirit do vnitrni site. Stejne pravidlo jako u HTTP (isPrivateHost), protoze adresu vyplnuje firma.

2026-08-26 - katalog stoji na sluzbach, ktere opravdu bezi, a pribyla OpenAI

Katalog popisoval, co chceme umet. U vetsiny sluzeb chybely pristupove udaje, takze konektor nemel co vyplnit, a appId u nekterych ukazovalo na aplikaci, ktera neexistuje. Zdroj pravdy o tom, co bezi, je services.csbot.cz/apps a jeji /openapi.json.

Novy dokument: 21-realne-sluzby.md.

Zmeneno

  • Trinact sluzeb katalogu ma napojeni na bezici aplikaci vcetne poli udaju a levneho cteciho volani na overeni. RAYNET, CSOB, PPL, Microsoft 365, GA4, Search Console, Google Ads, Sklik a Prepis hovoru mely credentials: [], takze konektor nesel vyplnit a test se nemel ceho chytit.
  • Opravena appId, ktera nikam nevedla. PPL bezi na pplcplapi, ne na ppl. Microsoft 365 na microsoft-365-service. Prepis hovoru na audio-transcription. GA4, Search Console, Google Ads a Sklik bezi vsechny na aplikaci analytics, kazdy s vlastnimi udaji - slucovat je do jedne sluzby by znamenalo, ze firma se samotnym Sklikem musi vyplnit i Google.
  • Prepis hovoru ma jen operaci, kterou aplikace umi. Operace "Vytvorit souhrn" za sebou nic nemela; souhrn ted udela OpenAI.
  • Query ve verifyPath se do hlasky nedava. Odrizne se stejne jako u ScriptRequestInfo - v query muze byt tajemstvi.

Pridano

  • Sluzba OpenAI. Firma zada svuj API klic do konektoru a muze se ptat modelu, poslat soubor a nechat si prepsat zvuk. Skripty openai.chat, openai.upload-file, openai.ask-about-file, openai.transcribe-audio a openai.list-models.
  • Sluzba, ktera nebezi u nas. Service.baseUrl nese absolutni adresu cizi sluzby. Skladat ji ze SERVICES_BASE_URL by nedavalo smysl, to je zaklad nasich aplikaci. Prepsat ji jde promennou <SLUZBA>_BASE_URL (dosud jen slibenou v dokumentaci, ted opravdu implementovanou) nebo adresou u konektoru, cimz vede cesta na Azure OpenAI.
  • Predpona hlavicky u pole udaju (ServiceCredentialField.prefix). OpenAI chce Authorization: Bearer <klic>; uzivatel vlepi holy klic a slovo pred nim dopise runtime. Preklep v rucne psanem Bearer by nesel najit ani zpetne, protoze se hodnota z API nevraci.
  • Odesilani souboru ze skriptu (ctx.http.postForm). Obsah prichazi jako Base64, protoze parametr kroku se uklada do zaznamu behu a binarni data by se tam nevesla. Strop SCRIPT_MAX_UPLOAD_BYTES, vychozi 10 MB.
  • Sluzby SAP Business One, Google Workspace a Meta Ads. Bezi, ale v katalogu nebyly. Meta Ads je zamerne oddelena od Facebook Messengeru a Instagramu: aplikace meta je nad Marketing API, tedy reklamy, a je jen pro cteni.
  • Dvacet tri skriptu k realnym sluzbam, od dohledani firmy v RAYNETu po vykon kampani ve Skliku. Seznam je v 21-realne-sluzby.md.

Opraveno

  • Redakce tajemstvi bere i holou hodnotu. Dosud se skrtaly jen hodnoty hlavicek, takze u Authorization: Bearer sk-... by samotne sk-... v chybove odpovedi proslo do logu. targetSecrets ted vraci obojí a pouziva ho i overeni konektoru, ktere si dosud sestavovalo seznam samo.

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.

  • Odchozi IP adresa portalu (src/data/egressIp.ts, endpoint GET /api/dashboard/connectors/egress-ip). Z containeru neni videt, jak ho protistrana vidi, takze pri 403 od seznamu povolenych IP nebylo co porovnat. Adresa je natvrdo na strance Konektory a u kazdeho odmitnuteho overeni v logu. Zjistuje se echo sluzbou podle EGRESS_IP_URL, drzi se v pameti po EGRESS_IP_TTL_MS, u 401 a 403 se pripoji k zaznamu. Prazdna EGRESS_IP_URL funkci vypne.

  • Druhe mereni: jak nas vidi nase vlastni proxy. Echo sluzba odpovi verejnou adresu, jenze volani na vlastni domenu se otaci zpatky na tentyz stroj a proxy pak vidi neco jineho, typicky adresu docker bridge. A prave tu porovnava seznam povolenych IP u sluzeb za toutez proxy. Portal proto zavola svoji vlastni verejnou adresu (PUBLIC_ORIGIN + ROOT_PATH + /whoami) a precte si, jak k nemu volani doslo. Novy endpoint /api/whoami je bez prihlaseni: vraci volajicimu jeho vlastni adresu, nic navic.

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.

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: 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.

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 a vsechna data se ukladaji. Rejstrik novych funkci a komponent je v 15-rejstrik-funkci.md.

Pridano - obecne vrstvy

  • src/data/store/: jedno rozhrani EntityStore<T> 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.

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.

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.

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.

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.

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 a 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.

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.

  • 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.

  • 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.

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 <base> 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 <base>,
  • 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.