Commit Graph
45 Commits
Author SHA1 Message Date
JiriUhlirandClaude Opus 5 d71c111cf3 Jazyky, skutecny snimek v heru a opravene titulky
Prepinani jazyku (web/src/i18n/). Vybrany jazyk je jedna hodnota pro celou
aplikaci drzena mimo React - tentyz vzorec jako u vybrane firmy, a ze
stejneho duvodu: musi byt dostupny i mimo komponenty a prepnuti nesmi byt
stav jedne stranky. Volba prezije obnoveni a prepisuje <html lang>.

Anglicky slovnik je Partial: co v nem neni, spadne na cestinu. Kdyby musel
byt uplny, znamenal by kazdy novy cesky text rozbity build, dokud ho nekdo
neprelozi - a vysledkem by bylo, ze se texty nepridavaji. Prelozena je
hlavicka, hero vcetne snimku, pas s logy, cisla, zaverecna vyzva a cast
paticky; zbytek je mechanicky a popsany v 23-jazyky.md.

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 zmenilo brand.ts a titulky ne:
hlavicka rikala nove jmeno a zalozka v prohlizeci stare.

Opraveno: odkaz "Prohlednout si to" vedl na koren domeny misto
/apps/<app-id>/sluzby, protoze to byl <a href> a ne Link z routeru. Byl to
jediny takovy odkaz v celem webu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 10:37:58 +02:00
JiriUhlirandClaude Opus 5 33b4a3fdd6 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 - prave to nas dnes stalo pul hodiny hadani, proc je na produkci
porad stary vzhled.

- oznaceni buildu v pate postranniho menu, pod tlacitkem Odhlasit se:
  verze, cas buildu a cislo commitu. Text jde oznacit jednim kliknutim.
- 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 presne to clovek potrebuje pri zjistovani, co bezi
  na produkci.
- hodnoty dosazuje Vite pri buildu (define a plugin build-stamp), takze
  neplati pro repozitar, ale pro ten konkretni balicek.
- .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 a kopiruje se do nej jen
  dist, scripts a migrace.

Ve vyvoji se misto casu pise "vyvoj" - cas buildu by tam byl pokazde jiny
a nikomu by nic nerekl.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 10:10:34 +02:00
JiriUhlirandClaude Opus 5 73e0ecca4f Rebrand na WorkNuke: zelena znacka a prestavene hero
Prevzeti vizualniho smeru z predlohy. Web i portal stoji na tychz tokenech,
takze zmena hodnot v index.css se projevila naraz na obojim - portal je
zeleny, aniz by se sahlo na jedinou jeho komponentu.

Znacka a tokeny:
- akcent z cyanove na zelenou #B7FF3C, podklad z modrocerne na zelenocernou
  #0F1411. Seda dostala zelenou prichut, aby vedle podkladu nepusobila spinave.
- druhy akcent a text-gradient se nemazaly, jen presmerovaly do zelene rady.
  Pouziva je 341 mist a prepsat je vsechna kvuli barve by byla zbytecne velka
  zmena; fialova z nich zmizela tak jako tak.
- stav "v poradku" je nove 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 je plocha bomba misto pismene v gradientnim ctverci. LogoMark je
  samostatny export, aby sel pouzit i uvnitr snimku v heru. Novy favicon.
- primarni tlacitko je plna zelena misto prechodu ze dvou barev.

Hero a navigace:
- 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.

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.
- presah snimku se pocita ze sirky kontejneru. Pevna hodnota ve vw stacila
  na notebooku, ale nad 1600 px se snimek vesel do postranni mezery
  a prestal byt oriznuty.

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

Popis, jak znacka funguje v kodu, je v documentation/22-znacka-a-design.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:56:23 +02:00
JiriUhlirandClaude Opus 5 2e863c1f8b Prepinac firmy do sidebaru a prepnuti prekresli obsah cely
Dodelavka. Tvrzeni "prepinac plati pro cely system" nebylo pravdive:
useApiQuery se na zmenu firmy zeptat umel, ale komponenty volajici
apiFetch primo - EntityAdmin, a tim cele Nastaveni a zalozky Resitele
a Skupiny v Lidech - o zmene nevedely a zustala na nich data predchozi
firmy. Presne ta vada, kvuli ktere se to delalo, jen o patro vedle.

- obsah dashboardu ma key podle vybrane firmy, takze prepnuti prestavi
  cely strom komponent. Zadny dotaz nemuze zustat stary, protoze zadna
  stara komponenta nezustane. Doplnovat zavislost do kazde komponenty
  zvlast je totez zapomenuti, jen o patro niz.
- prepinac je v sidebaru nad navigaci, ne v hlavicce, a vypada jako
  ovladaci prvek: ram, ikona firmy, sipka. Predchozi verze mela pruhledny
  ram a splyvala s popiskem.
- v hlavicce zustal jen nazev aktivni firmy. Driv tam stalo natvrdo
  tenants[0], takze po prepnuti ukazovala porad prvni firmu ze seznamu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 08:04:22 +02:00
JiriUhlirandClaude Opus 5 2e0b9456a9 Prava plati za firmu, ne za cloveka
permissionsOf(user) scitalo role pres vsechna clenstvi, takze kdo byl
spravce v jedne firme, jednal jako spravce ve vsech, kam patril. Uzivatel
ve dvou firmach je vzacny, dusledek ne.

- firma je povinny argument u permissionsOf, hasPermission i hasAnyPermission.
  Zamerne povinny: kdyby byl nepovinny, prvni volani bez nej by tise vratilo
  vsechna prava. Prekladac tim rovnou nasel vsech ctrnact mist, ktera se ptaji.
- kazde misto se pta za tu spravnou firmu: akce nad ticketem za firmu toho
  ticketu, uprava zaznamu za firmu toho zaznamu, zalozky a helpdesk za firmu,
  kterou ma clovek prepnutou
- cizi firemni role se ignoruje, i kdyby na ni clenstvi odkazovalo. Jinak by
  stacilo pripsat si ji do clenstvi v jine firme a prava by se prenesla.
- cache prav ma v klici firmu
- readScope cte z jedne firmy, te prepnute, ne ze vsech. V nastaveni se driv
  michaly typy ticketu a role napric firmami bez ohledu na vyber.

Opraveno u toho: systemove role se pri startu srovnaji s kodem
(syncSystemRoles). Vychozi sada se pouzije jen do prazdneho uloziste, takze
nove pravo v katalogu se k uz bezici instalaci nikdy nedostalo - ticket.create
ani obe prava helpdesku by spravci firmy nenabehla a zalozka Helpdesk by se
neobjevila. Meni se jen prava, jen u nasich roli, nazvu se to netyka.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 07:57:01 +02:00
JiriUhlirandClaude Opus 5 2329ccbdc1 Prepinac firmy plati pro cely portal, ne jen pro prehled
Prepinac byl stav uvnitr stranky Prehled, takze prepnuti na LogiTrans
zmenilo prehled a nic jineho - ostatni stranky volaly API bez tenantId
a server sahnul po vychozi firme. V Lidech tak zustali lide Automie.

- vybrana firma je jedna hodnota pro cely dashboard (web/src/lib/tenant.ts)
  a prezije obnoveni stranky
- tenantId doplnuje apiFetch, ne jednotlive stranky. Kdyz si to mela
  pridavat kazda stranka sama, vetsina na to zapomnela - a prave to byla
  ta chyba. Nedoplnuje se, kdyz cesta uz firmu nese, u scope=all
  a mimo /api/dashboard.
- useApiQuery se pri prepnuti zepta znovu, jinak by na strance zustala
  cisla predchozi firmy
- prepinac je v hlavicce nad obsahem a nahradil text, ktery ukazoval
  natvrdo prvni firmu ze seznamu bez ohledu na vyber
- ulozena firma, do ktere uzivatel uz nepatri, spadne na vychozi

Zjisteno u toho a zapsane do 07-firmy-a-prava.md: prava se scitaji pres
vsechny firmy, neprepocitavaji se podle vybrane. Data se tim neprolomi,
server je filtruje dal, ale tlacitka ukazuji vic, nez by mela.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 07:48:37 +02:00
JiriUhlirandClaude Opus 5 a771834e57 Realne sluzby, OpenAI, odesilani e-mailu a helpdesk
Katalog srovnany s tim, co opravdu bezi na services.csbot.cz/apps:
trinact sluzeb dostalo pristupove udaje a levne cteci overeni, opravena
appId, ktera nikam nevedla (ppl, microsoft365, transcription), a GA4,
Search Console, Google Ads i Sklik ted stoji na aplikaci analytics,
kazda s vlastnimi udaji. Nove sluzby SAP Business One, Google Workspace
a Meta Ads. K tomu 23 skriptu, ktere s nimi opravdu neco delaji.

OpenAI jako prvni sluzba, ktera nebezi u nas: Service.baseUrl s absolutni
adresou, prepis pres <SLUZBA>_BASE_URL nebo adresu u konektoru, predpona
hlavicky u pole udaju (uzivatel vlepi holy klic, Bearer dopise runtime).
Dotaz na model, nahrani souboru, otazka nad souborem, prepis zvuku.
Skript umi odeslat soubor pres ctx.http.postForm (multipart, obsah Base64).

Sluzba E-mail pres SMTP. Neni to skript, ale vnitrni krok - SMTP neni HTTP.
Konektor nese schranku firmy, krok ma HTML telo, ve kterem se dosazene
hodnoty escapuji (znacky autora sablony jsou zamer, ostre zavorky od
zakaznika ne). Overeni konektoru se prihlasi na server a nic neodesle.

Helpdesk: Ticket.helpdeskSourceId drzi firmu, ktera pozadavek poslala,
vlastnikem zustava ta, ktera ho resi - jinak by ho resitel nemel ve sve
fronte. Komu pozadavek pripadne, urcuje Tenant.helpdeskProviderId.
Zadavatel vidi jen svoje pozadavky a smi k nim pripsat komentar.

Opravy v portalu:
- hlasky o ulozisti a odchozi IP vidi jen spravce platformy
- typ ticketu se v automatizaci vybira ze seznamu firmy, nebo dosadi z dat
- stav ticketu je otevreny naseptavac, ne ciselnik
- ticket jde zalozit rucne, zakaznik u nej neni povinny
- kanal se prejmenoval a parametry u webhooku jsou oznacene jako nepovinne
- srovnane markdown tabulky v cele dokumentaci

Co z teto davky jeste neni: prepinac firmy je porad jen stav uvnitr stranky
Prehled, takze se prepnuti neprojevi v Lidech ani jinde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 07:40:16 +02:00
JiriUhlirandClaude Opus 5 b25a149574 Ke zmerene adrese i rozsah, ktery ji pokryje cely
Povolit jednu adresu nema smysl: docker prideluje z bloku a pri prekresleni
site nebo redeployi se cisla meni, takze povoleni vydrzi do prvniho restartu.
Portal proto k namerene adrese dopocita CIDR rozsah - 127.0.0.0/8, 10.0.0.0/8,
192.168.0.0/16, 172.16.0.0/12, 169.254.0.0/16, ::1/128, fc00::/7, fe80::/10.

U 172.16-31 schvalne /12 a ne /16 toho konkretniho bridge: docker si smi vzit
kterykoliv podblok a nikdo nezaruci, ze zustane u toho dnesniho.

Verejna adresa zadny rozsah nedostane, tam nabizet blok nema smysl.

Taky opravena popiska u remoteAddress. Je to sama proxy, ne volajici -
k nam uz to jde od ni.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:37:06 +02:00
JiriUhlirandClaude Opus 5 6a2e0dc424 Zmerit i to, jak nas vidi vlastni proxy
Echo sluzba na internetu odpovi verejnou adresu. Jenze volani na vlastni
domenu se otaci zpatky na tentyz stroj a reverse proxy pak vidi neco jineho,
typicky adresu docker bridge. A prave tu porovnava seznam povolenych IP
u sluzeb za toutez proxy, takze verejna adresa muze byt povolena a volani
z containeru presto skonci na 403.

Zmeri se to tak, ze portal zavola svoji vlastni verejnou adresu
(PUBLIC_ORIGIN + ROOT_PATH + /whoami) a precte si, jak k nemu volani doslo.
Kruh sam pres sebe, ale nic jineho tuhle adresu nezjisti: mezi container
a server se tim dostane ta sama proxy, kterou prochazi volani na sousedni
sluzby.

Novy /api/whoami je zamerne bez prihlaseni. Vraci volajicimu jeho vlastni
adresu, tedy nic, co by uz nevedel, stejne jako kterakoliv echo sluzba.

Obe mereni bezi naraz a jsou videt na strance Konektory vedle sebe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:33:46 +02:00
JiriUhlirandClaude Opus 5 4545de8076 Odchozi IP adresa portalu
Kdyz cizi sluzba odmitne pristup, prvni otazka je, z jake adresy se vlastne
vola. Z containeru to videt neni, vidi to az protistrana, takze se zepta echo
sluzby podle EGRESS_IP_URL a vysledek se drzi v pameti po EGRESS_IP_TTL_MS.
Prazdna EGRESS_IP_URL funkci vypne, prepsat ji jde na vlastni echo pod svou
domenou.

Adresa je natvrdo na strance Konektory a u kazdeho odmitnuteho overeni v logu.
Pripojuje se jen u 401 a 403 - jinde nema co rict a nestoji za volani ven.

Neni to tajemstvi: kazda volana sluzba tuhle adresu stejne vidi.

Endpoint egress-ip je registrovany pred GET /:id, jinak by ho router vzal
jako id konektoru.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:22:26 +02:00
JiriUhlirandClaude Opus 5 ab88979627 Dialog portalem do body, hlavicky odpovedi u chyby
Dialog se vykresloval uvnitr karty konektoru misto pres obrazovku. Samo
position: fixed nestaci: rodic s backdrop-filter (nase .glass, tedy skoro
kazdy panel a karta) je pro fixed potomka containing block. Modal proto jde
portalem do document.body. Tykalo se to vsech dialogu, videt to bylo az
u Logu, ktere jsou v male karte.

K chybe se zapisuji vybrane hlavicky odpovedi: server, via, content-type,
www-authenticate, retry-after, x-request-id, date. Rikaji, kdo odpoved vydal.
Server: Kestrel je sama aplikace, Via: 1.1 Caddy proxy pred ni. U 403 od proxy
byva telo prazdne a bez hlavicek by nezbylo vubec nic. Allowlist, ne vsechno:
Set-Cookie a podobne do zaznamu nepatri.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:15:22 +02:00
JiriUhlirandClaude Opus 5 5a124a53d8 Chybova hlaseni konektoru rikaji, co se stalo, a kam to slo
Duvod od sluzby jde primo do hlasky: z tela odpovedi se vytahne detail,
error_description, message, title, error i seznam missingHeaders. Retezec,
ktery vypada jako JSON, se rozbaluje dal - sluzba iDoklad presne takhle
predava telo od iDokladu samotneho. Kdyz sluzba nenapsala nic, rekne se to.

401 a 403 uz nejsou jedna hlaska. 401 = udaje sluzba dostala a neuznala je.
403 = tvar udaju je v poradku, zakazuje se samo volani.

V kazde hlasce je cela adresa vcetne serveru (ScriptRequestInfo.url),
bez query - v query muze byt tajemstvi. Zaklad adresy je z konfigurace
a konektor ho smi prepsat, takze se neda odvodit z toho, kde je nasazeny
portal. Adresa je videt i na karte konektoru a v odpovedi na test, i kdyz
overeni projde.

Tlacitko Logy na karte konektoru a historie poslednich peti overeni.
Odpoved sluzby dosud existovala jen v odpovedi na test, tedy do prekresleni
stranky, a v logu containeru. Do logu containeru se nikdo divat nechodi.
Zaznam se uklada i pri uspechu, jinak by neslo poznat, jestli konektor nesel
nikdy, nebo prestal jit ve chvili, kdy nekdo sahnul na udaje.

Migrace 003_connector_checks.sql, endpoint GET /connectors/:id/checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 07:06:14 +02:00
JiriUhlirandClaude Opus 5 6297bbf480 Vystup z vetve plati i za podminkou, samostatny krok Zalozit kontakt
Bezny postup nesel poskladat z kroku: najdi podle ICO, kdyz neni zkus mail,
kdyz porad neni zaloz - ID dava jednou jedna vetev a jednou druha, ale
rozsah vystupy z vetvi za podminku nepoustel. Slucovat kvuli tomu hledani
a zakladani do jednoho kroku bylo obejiti nasi chyby, ne reseni.

- Vystup z vetve je za podminkou k dispozici, jen jako nepovinny. Ze muze
  chybet, se neztratilo: builder to u pole ukaze a pri behu se dosadi
  prazdno.
- Vystup se stejnym jmenem uz z nabidky nemaze ten starsi. Po druhem
  hledani kontaktu zmizelo ID z prvniho, tedy to, co je v tu chvili
  potreba. Odkaz se jmenem kroku je jednoznacny.
- Duplicitni jmena u vystupu kroku uz nejsou nedodelek. Konflikt zustava
  mezi parametry spoustece, kde zadny prefix neni.
- Novy krok Zalozit kontakt. Nic nedohledava, hledani je vlastni krok.
  Najit nebo zalozit zustava pro toho, komu staci jistota jednim krokem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 09:01:57 +02:00
JiriUhlirandClaude Opus 5 c62902bfb6 Polozky dokladu se najdou pod obema jmeny
iDoklad ceka v tele `IssuedInvoiceItems`, ale prevod je casto pojmenuje
`items` podle toho, jak se jmenuji ve zdrojove objednavce. Kontrola trvala
na `items`, telo se ale posilalo tak, jak prislo - prevod s PascalCase
jmeny by tedy neprosel kontrolou, a prevod s malymi pismeny by prosel
a doklad by v iDokladu vznikl bez radku.

Ted se polozky hledaji pod obema jmeny a do tela se doplni pod tim, ktere
ceka iDoklad.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:53:24 +02:00
JiriUhlirandClaude Opus 5 13c0c03ed4 Krok Najit nebo zalozit kontakt a druhy vstup vlastniho skriptu
Chybelo k tomu, aby sla postavit cela cesta objednavka -> faktura.

- Novy skript idoklad.upsert-contact: dohleda odberatele podle ICO nebo
  e-mailu, a kdyz neni, zalozi ho a vrati nove ID. Vystup `created` rekne,
  co se stalo, takze na to jde navazat podminkou.
  Proc jeden krok a ne dvojice ve vetvich podminky: vetev nepridava nic do
  sekvence za podminkou, protoze nemusela probehnout. ID odberatele by za
  podminkou nebylo k dispozici, i kdyz ho ve skutecnosti daji obe vetve.
  Zaklada se az kdyz hledani nic nenaslo - opacne poradi by pri kazde
  objednavce vyrobilo dalsiho odberatele se stejnym ICO.
- Krok Vlastni skript ma druhy vstup "Co pridat ke vstupu". Skript casto
  potrebuje krome hlavniho objektu jednu hodnotu z predchoziho kroku,
  typicky ID odberatele. Prijde v input.extra, aby se hlavni data nemichala
  s tim, co jsme doplnili my.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:50:03 +02:00
JiriUhlirandClaude Opus 5 e4779caf82 Vlastni skripty firmy: prevod dat v JS, v logu vstup i vystup
Klikaci pravidla jsou u peti poli rychlejsi, ale u modelu objednavky je jich
dvacet a v tom se necte. Vedle nich proto skript firmy: prevod z A do B
napsany v JS, jeden na zakaznika.

- Skripty se ukladaji do uloziste, ne na disk. Disk je uvnitr kontejneru
  a redeploy ho vymaze.
- Krok Transformace dat - Vlastni skript. Vysledek jde dal jako krok.result.
- V logu ticketu je u kroku vstup i vystup. Prave to byl duvod, proc skript
  nad pravidly vyhral.
- Zkouska bez ulozeni: v portalu se vlepi skutecne telo a hned je videt, co
  z toho leze.
- Skripty jsou v zalozce Akce, vedle definic akci. Obojí je popis toho, co
  aplikace ve firme umi, a spravuje to tentyz clovek.

Skript je ciste prevod hodnot: dostane input, vrati objekt. Nema require,
import, process, fetch ani console, bezi nejvys 2 s a vysledek se vejde do
256 kB. node:vm neni bezpecnostni hranice proti nekomu, kdo se chce dostat
ven - je to izolace proti nehode a proti zacykleni.

Pri zkousce se ukazalo, ze casovy limit nepokryval samotny beh: runInContext
jen vyrobil funkci a zavolat ji zvenku znamenalo, ze while (true) uvnitr
zablokovalo proces navzdy. Kod se ted vola uvnitr runInContext.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:08:09 +02:00
JiriUhlirandClaude Opus 5 99561c4682 Pauza, zapis do logu a podminky nad vystupy konecne funguji
Vyslo najevo pri stavbe ukazkovych automatizaci nad modelem objednavky.

- log/write a delay/wait nemely vykonnou cast. Byly registrovane jen pod
  klici flow/log a flow/pause, ktere v katalogu nejsou. Kdo si v builderu
  vybral Zapsat zpravu nebo Pockat, dostal pri behu "operace nema vykonnou
  cast" - krok slo pridat, ale nikdy nefungoval.
- Obe operace navic nemely nastavitelna pole, takze do nich neslo napsat,
  co se ma zapsat a jak dlouho cekat.
- Podminka nad vystupem kroku se nevyhodnotila. Podminka si drzi ID
  parametru, aby ji prejmenovani nerozbilo, ale data chodi pod jmenem:
  parametr f_9x1 nese hodnotu z klice callSid, vystup kroku
  st_kontakt.idoklad.found lezi pod found. Beh dostava tabulku jmen podle
  ID, takze se ma o co oprit. Bez toho podminka tise vychazela jako prazdna
  a slo se vzdy vetvi NE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 07:04:30 +02:00
JiriUhlirandClaude Opus 5 9b18531d3e Prace nad celym modelem: cesty, ukazka tela a smycka nad seznamem
Odesilatel posila cely model. Objednavka ze Shoptetu ma zanoreni, ceny
v podobjektech a seznam polozek - a dosud sel napojit jen plochy seznam
skalarnich poli, takze items[] neslo pouzit vubec.

- Odkaz v sablone muze byt cesta: {{data.order.billingAddress.city}},
  {{data.order.items[0].name}}, {{st_faktura.invoiceId}}. Overuje se prvni
  cast odkazu, takze ploche odkazy funguji dal presne jako driv.
- Cele telo je v kontextu i v puvodnim tvaru. Deklarovane parametry maji
  pri shode jmen prednost.
- Spoustec si pamatuje ukazku skutecneho tela. Server z ni odvodi model,
  tedy seznam cest i s typy, a ten se v krocich klika misto opisovani.
  Tlacitko doplni z hodnot v ukazce parametry pro podminky.
- Novy krok Pro kazdou polozku: projde seznam a za kazdou polozku vykona
  vnoreny podstrom. Uvnitr je item a index, po skonceni krok.results se
  seznamem vysledku. Kazdy vysledek nese i puvodni polozku - radek
  objednavky potrebuje jak ID z CRM, tak mnozstvi z puvodnich dat.
  Strop 200 polozek, mimo seznam krok selze s tim, co tam misto nej je.
- Prevod Za kazdou polozku seznamu v klikacim editoru mapovani. Engine ho
  umel, sel ale napsat jen rucnim JSONem.
- Typograficke uvozovky a sipka z kodu pryc.

Overeno nad skutecnym modelem objednavky: 23 cest vcetne
data.order.items[].unitPrice.withoutVat, sablony s cestou i s indexem,
smycka nad dvema polozkami s posbiranymi vysledky.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 06:53:10 +02:00
JiriUhlirandClaude Opus 5 fc0c2ff6c7 Opakovani do logu nepise a ticket se rodi s poslanym stavem
Overeno proti serveru: tri shodne POSTy udelaly jednu udalost s pocitadlem 3,
ale krok se do logu porad zapsal trikrat. Zadani bylo, ze opakovani ma byt
informace, ne dalsi radek.

- Krok muze rict quiet a jeho radek se do logu ticketu nezapise.
- Ticket vznika uz s poslanym stavem. Predtim se zalozil s vychozim "Novy"
  a hned se prepsal, takze v logu stalo "stav Novy -> completed" u ticketu,
  ktery v tom stavu nikdy nebyl. Odtud i to "Novy" ve widgetu.
- Zaznam zmen: celkovy pocet behu se pocita od zavedeni historie po dnech,
  puvodni citac se den ode dne nedelil a rozpocitat ho zpetne neni z ceho.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:14:20 +02:00
JiriUhlirandClaude Opus 5 f1e8253169 Log rekne co zpusobilo jakou zmenu, prevzeti ticketu a pozvanky
Nalezeno na bezicim serveru: TK-4946 mel 177 udalosti a 620 radku logu,
pritom se skoro nic nestalo. Zmereno proti fronte: ve stejnem okne vzniklo
presne tolik behu, kolik prislo udalosti (22 a 22), kazdy s jednim pokusem.
Fronta nenasobi nic, odesilatel poslal 177 POSTu. Nase vina byla, ze to
z historie neslo poznat.

- Data udalosti se ukladaji. Kdyz krok nema vlastni, ulozi se to, cim beh
  zacal - u webhooku prijate telo. Prazdna udalost je horsi nez zadna.
- Shodna udalost se pocita (repeats, lastAt), nezaklada dalsi radek. Ticket
  se pritom nemeni, takze duplikat nerozblika dashboard ani nespusti
  automatizaci na zmenu ticketu. Zahodit ji nejde, jinak by nikdo nezjistil,
  ze proti nam neco tluce.
- Zmeny se radi pod udalost, ktera je zpusobila, a u udalosti stoji jmeno
  automatizace. Log se cte jako "prislo tohle -> zmenilo to tohle".
- Poznamka o stavu jen kdyz se stav zmenil. "z in-progress na in-progress"
  u kazde zpravy byl zdroj tech 620 radku.
- runsToday konecne znamena dnes: behy po dnech, k tomu vcera a celkem.
  Dosud to byl citac od zalozeni automatizace, jen se jmenoval "dnes".

Vedle toho prace, o kterou slo predtim:
- Prevzeti ticketu ze skupiny (POST /tickets/:id/claim) a krok Predat skupine
  s prepinacem automatickeho prideleni nejvolnejsimu.
- Pozvanky do firmy: odkaz s nahodnym kodem, heslo si nastavi pozvany.
- Resitele, skupiny a pozvanky presunuty z Nastaveni do zalozky Lide, cleny
  skupiny se vybiraji klikanim.
- Ctyri AI znaky, ktere zbyvaly v kodu, pryc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:54:34 +02:00
JiriUhlirandClaude Opus 5 fbe5b8ff6f Vyrizeno je vyslovny priznak, ne hadani ze stavu
`Ticket.closed` se nastavuje vyslovne. Predchozi verze ho odvozovala ze jmena
stavu (completed, vyreseno, ...), coz nikdo nechtel a hlavne to uhodne spatne
pokazde, kdyz si nekdo pojmenuje stavy po svem. `TicketType.closedStatuses`
zruseno, byla to tatáz obchazka o uroven vys.

Kroky `ticket/upsert` a `ticket/set-status` maji vstup Vyrizeny s trema stavy:
ano, ne, prazdne. Prazdne znamena nemenit - jinak by kazda zmena textu stavu
mimochodem otevrela vyrizeny ticket. `POST /tickets/:id/status` prijima
`closed` jako nepovinny bool a podminka ve strome se na nej muze zeptat.

Overeno 8 kontrolami: ticket se stavem completed neni automaticky vyrizeny,
dokud to nekdo nerekne. Automatizace s podminkou status = completed zabere na
ticketu, ktery do toho stavu prejde, ale na uz existujici tickety nesahne -
spousti ji udalost, ne stav.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 07:38:20 +02:00
JiriUhlirandClaude Opus 5 202d525905 Stav ticketu je volny retezec, ciselnik pryc
Ciselnik new/open/waiting/resolved je pryc. Tickety chodi z cizich aplikaci,
ktere maji svoje stavy - voicebot posila ringing a completed. Nutit je do nasi
ctverice znamenalo, ze u ticketu svitilo "Novy", i kdyz byl podle odesilatele
davno hotovy.

Misto nej priznak `closed`: fronta, vytizeni i statistiky potrebuji vedet, co
uz nikdo neresi, a z volneho retezce to poznat nejde. Nastavuje se sam podle
`TicketType.closedStatuses`, a kdyz je typ nema, podle bezneho pojmenovani
(vyreseno, hotovo, completed, closed).

`stage` zruseno. Byla to obchazka, jak dostat cizi stavy do ticketu, aniz by
se sahlo na ciselnik. Kdyz je stav volny, druhe pole na tutéz vec jen matlo.

Vyber stavu v detailu nabizi stavy typu, doporucene a ten, ktery ticket ma
prave ted, aby hodnota z cizi aplikace ze seznamu nezmizela. Filtr v seznamu
nabizi stavy, ktere v datech opravdu jsou.

Overeno 11 kontrolami: ticket z voicebota ma stav ringing, pak in-progress
a completed, completed se pozna jako hotovo a zmizi z fronty, filtr i widget
ukazuji tvoje stavy a rucne jde nastavit i "ceka na zpetne volani".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:36:41 +02:00
JiriUhlirandClaude Opus 5 ef3080d3f6 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 trema vecmi:

Ukazkova data jen se SEED_DEMO=1. Automatizace, tickety a incidenty se uz po
kazdem nasazeni nevraci. Konfigurace se nasypava dal, bez ni je portal
nepouzitelny.

Token webhooku z WEBHOOK_TOKEN_TEST. Driv se pri kazdem nasazeni vygeneroval
novy, takze odesilatel musel prepisovat adresu ve svem kodu. Ted je token
v promenne aplikace: neni v gitu a adresa se nemeni.

Automatizace, ktera na instanci opravdu bezi, je v seedu. Je to provizorium,
ne cil - az data prezijou nasazeni, patri zpatky do dat.

Dal:

- `ticket/upsert` umi vsechna pole ticketu: zakaznik, kanal, odkaz na zdroj,
  priorita, stitky, resitel, skupina a vlastni pole typu jako JSON. Zakaznik
  a kanal se vyplnuji jen pri zalozeni, aby pozdejsi udalost s prazdnym
  jmenem neprepsala, co uz tam je.
- Kroky ve strome jdou sbalit, vychozi je sbaleno. Sbaleny krok ukazuje, co
  ma vyplneno, ne popis operace.
- Seskupovani widgetu podle faze a dva nove widgety: tickety podle stavu
  a podle faze za tento mesic, obojí s proklikem na vyfiltrovany seznam.
- Faze se ukazuje jako stav. Driv byl videt jen nas ctyrprvkovy ciselnik,
  coz u ticketu z cizi aplikace nedava smysl. Zivotni cyklus zustava vedle
  jako drobny text, protoze se z nej pocitaji statistiky.

Overeno 18 kontrolami proti bezicimu serveru.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:21:32 +02:00
JiriUhlirandClaude Opus 5 6abac82d5e Krok prirazeni resiteli a ukazkova data mimo zivy provoz
Opraveno: `ticket/assign` nemel vykonnou cast, takze krok "Prirad resiteli"
vzdycky selhal hlaskou "operace nema vykonnou cast". Doplnen jako vnitrni krok
vedle prirazeni nejvolnejsimu ze skupiny a prirazeni podle externiho ID.

Ukazkova automatizace "Smerovani ticketu na resitele" je vypnuta. Zapnuta
prebirala tickety, ktere uz nekomu patrily podle skutecne automatizace
zakaznika, a prepsat rucni nebo cizi rozhodnuti je to nejhorsi, co muze
automatizace udelat. Do udaju spoustece zaroven pribylo `assigned`
a `knownCustomer`, aby slo napsat podminku "uz je prirazeny, nesahej na to".

`ticket/upsert` prijima `status`: kdyz hodnota patri mezi nase ctyri stavy,
nastavi stav, jinak se ulozi jako faze. Cizi aplikace posila svoje stavy
hovoru a nas zivotni cyklus je pevny, protoze se z nej pocitaji statistiky.
Do shrnuti kroku se napise, co se stalo.

Overeno pripadem z provozu: callSid do externiho ID, voicebotId jako stitek,
status jako stav. Ctyri zpravy o trech hovorech daly tri tickety, filtr na
stitek vratil jen hovory daneho voicebota a widget je spocital vcetne
prokliku na vyfiltrovany seznam.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:03:03 +02:00
JiriUhlirandClaude Opus 5 a57eca123e Fronta a worker: webhook odpovi hned, praci udelaji workeri
Webhook uz nic nevykonava v requestu. Zapise udalost do fronty a odpovi 202
do jednotek milisekund; strom vykona worker na pozadi. Za konektory nerucime,
takze cekat na cizi sluzbu v requestu znamena ztracet udalosti pri timeoutu.

Fronta ma opakovani s rostouci prodlevou (30 s, 2 min, 10 min, hodina),
spravedlive poradi po firmach (jedna firma s tisicem udalosti nezablokuje
ostatni), navrat zaseknutych behu po restartu a uklid hotovych. Marna chyba
se neopakuje - chybejici skript za minutu existovat nezacne.

Tri druhy spoustecu: push (webhook), vnitrni udalost (vznik a zmena ticketu)
a pull, tedy pravidelne dotazovani u sluzeb bez webhooku (posta, zpravy).
Planovac jen rekne "je cas", samotny dotaz je prvni krok stromu, takze ma
zaznam v logu a opakuje se pri chybe jako cokoliv jineho.

Kontrakt tela webhooku: kazdy parametr ma cestu (data.order.id,
errors.0.message), takze jde napojit i odesilatel s vnorenym modelem.
U adresy je metoda, ukazka tela a kopiruje se cela adresa vcetne domeny.

Vnitrni kroky, ktere sahaji do naseho uloziste: ticket/upsert (zaloz nebo
dopln podle externiho ID), assign-least-busy, assign-by-external, set-type,
set-stage, add-tags, set-status, incident/create, flow/pause a flow/log.

Faze ticketu jako treti osa vedle stavu a stitku. Stav je zivotni cyklus
a pocitaji se z nej statistiky, faze je workflow daneho typu a muze byt jen
jedna, takze se na ni da spolehnout v podmince.

ID z cizich aplikaci u resitele: voicebot posle voicebotId a ticket skonci
u toho, komu patri. Vazba je na jednom miste, ne v kazde automatizaci.

Kazda chyba zaklada incident se dvema urovnemi: impact cte klient a je
srozumitelny, detail cte admin a je v nem cely beh, ktery krok selhal, cele
hlaseni a data na vstupu. Detail vidi jen spravce platformy.

Ochrana proti smycce: automatizace navazana na zmenu ticketu ticket meni,
cimz se spousti znovu - pri vyvoji to server polozilo. Resi to oznaceni behu
pres AsyncLocalStorage a strop peti behu na jeden ticket za minutu.

Upozorneni pri prideleni prace vcetne cisla u zalozky Tickety. Zivy dashboard:
dlazdice nad nasimi daty na udalost, data z konektoru podle ttlSec s moznosti
vynutit nacteni znovu.

Opraveno: path a intervalSec u spoustece se pri ulozeni zahazovaly; nad
seznamem neslo pouzit contains, takze na stitky neslo postavit podminku;
novejsi vystup kroku ted prekryje starsi misto hlaseni konfliktu.

Overeno dvema scenari proti bezicimu serveru, 34 kontrol: firma se skladem,
expedici a IT, a hovory z voicebota (callSid do externiho ID, status do faze,
prirazeni podle voicebotId, tri zpravy = jeden ticket se tremi udalostmi).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 16:41:02 +02:00
JiriUhlirandClaude Opus 5 5d186dcd2e Runtime vykonava strom, prokliky z widgetu, oprava ukladani rozlozeni
Runtime: `src/runtime/executor.ts` jde krok po kroku, u podminky se vetvi,
do poli dosadi parametry, akci pusti pres runScript a vystupy pripise do
kontextu pro dalsi krok. Cely prubeh jde do logu ticketu vcetne toho, co
sluzba vratila. Pouzivaji ho obe cesty: akce na ticketu i webhook.

Opraveno: rozlozeni dashboardu s vlastnim widgetem se NEDALO ULOZIT.
`validateLayout` znala jen vestaveny katalog, takze kazdy pokus skoncil
hlaskou "widget v katalogu neexistuje" - presne to, co hlasil uzivatel.
Katalog je ted jedna funkce a pouziva ji nabidka i kontrola. Zaroven je
za konkretni firmu, driv slo polozit dlazdici jedne firmy na dashboard druhe.

Prokliky: z widgetu lidi na cloveka, ze seskupeni na vyfiltrovany seznam
ticketu. Odkazy sklada server, protoze on jediny zna filtr widgetu. Seznam
ticketu cte filtr z adresy a umi filtrovat na typ, tag a skupinu.

Tabulky: spolecna `TicketTable` pro seznam i detail osoby. Na mobilu se
neposouva do strany, uzka obrazovka dostane karty. Detail osoby ma velkou
tabulku se zalozkami "ma u sebe" a "vyresil" a prepinacem pohledu.

Odebrano: simulace vcetne tlacitka, dialogu i endpointu. Trojice pohledu
nad tickety - vyber firmy je select, "moje" je prepinac, driv to delalo
totez dvakrat.

Pridan zmereny rozbor kapacity pro 200 firem (19-kapacita-200-firem.md):
soucasny stav to nezvladne, protoze data jsou v pameti a vypis je linearni.
Zmereno na 5 000 ticketech, vcetne toho, co s tim a kolik serveru to chce.

Overeno 7 kontrolami proti bezicimu serveru.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:54:17 +02:00
JiriUhlirandClaude Opus 5 29de584df8 Ticketovaci system: udalosti, externi ID, statistiky a widgety nad konektory
Jakakoliv udalost se muze stat ticketem. Prijem je verejny endpoint na firmu
(`POST /webhook/ticket/:token`), takze zalozit ticket jde i bez stavby stromu.

Externi ID je unikatni V RAMCI FIRMY: dalsi zprava se stejnym ID se navesi na
existujici ticket misto zalozeni druheho, a stejne ID u jine firmy je jiny
ticket. Cislo a retezec jsou tentyz klic. Udalosti se drzi cele vcetne
prijatych dat a jdou rozbalit v detailu - je to neco jineho nez log.

Ticket nove nese firstResponseAt, resolvedAt, resolvedById a reopenCount.
Bez nich neslo rict, kdo kolik odbavil ani jak dlouho zakaznik cekal.
`getAgentStats` z toho pocita vykon resitelu vcetne medianovych casu
a vracenych ticketu. Pocet vyresenych sam o sobe odmenuje toho, kdo tickety
zaviral predcasne, proto je vraceni videt vedle nej.

Widgety: klient konecne vola /widget-data. Endpoint existoval, ale nikdo ho
nepouzival, takze vlastni widget hlasil "nepodarilo se zobrazit". Pribyl zdroj
`connector` - co umi zjistit napojena sluzba, jde vytahnout do dlazdice pres
tentyz skript, ktery pouziva krok automatizace. Vysledek se cachuje.

Akce a widgety uz nejsou v nastaveni, maji vlastni zalozku vedle automatizaci.
Telo akce se sklada stromem, ne JSONem v textarei - je to tentyz editor,
jen misto karty spoustece je "spousti clovek tlacitkem na ticketu".

Nova zalozka Lide se seznamem a detailem osoby. Seznam ticketu i lidi ma dva
pohledy, tabulku a dlazdice.

Opraveno: createTicket bral typeId, fields, tags i assigneeGroupId, ale nikdy
je neukladal. Ticket zalozeny s typem zustaval bez typu a bez vlastnich poli.

Dlouhe pomlcky, sipky, vypustky a bullety pryc z celeho projektu.

Overeno 21 kontrolami proti bezicimu serveru v rezimu souboru.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 15:13:09 +02:00
JiriUhlir 6c90372991 test 2026-08-13 10:20:47 +02:00
JiriUhlirandClaude Opus 5 afbe948da3 Uloziste pro vsechna data, oprava .gitignore, dodelany navrh rozsireni
.gitignore mel vzorec `data/`, ktery se shodl i se `src/data/`. Sestnact
zdrojovych souboru tim tise chybelo v gitu vcetne cele slozky
`src/data/store/`. Opraveno na `/data/`, stejne v .dockerignore.

Tickety vcetne logu, automatizace, incidenty a rozlozeni dashboardu se po
kazde zmene ukladaji. Pomocnik `withMirror` je opak `withCache`: data se meni
v pameti a zapisuji cela, misto aby se po zapisu znovu nacitala. Citace ID se
pri startu dopocitaji z ulozenych zaznamu, takze novy ticket neprepise stary.

Detail ticketu umi typ, tagy, vlastni pole typu a prehozeni na skupinu.
Nastaveni ma prepnuti spravce na jiny ucet, vychozi jen pro cteni.
Skupiny resitelu chodi spolu s lidmi jednim requestem.

Dokumentace: rejstrik znovupouzitelnych funkci (15), navrh monetizace
a ceny za krok (16), popis nastaveni a prav (17). Doplneny endpointy
do openapi.ts, petice CRUD rout se generuje jednou funkci.

Overeno v rezimu souboru: zmeny prezily tvrde ukonceni procesu a po restartu
byly zpatky vcetne logu ticketu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 07:46:22 +02:00
JiriUhlirandClaude Opus 5 e7cf499a0b Cely navrh rozsireni: role, firmy, typy ticketu, akce, widgety, audit
Implementace vsech bodu z documentation/09-navrh-rozsireni.md. Vsechno lezi
v obecnem ulozisti, ktere umi Postgres i JSON soubor.

Zaklad, aby se nepsalo osmkrat totez:
- src/data/store/: jedno rozhrani EntityStore, dve implementace (local se
  souborem nebo pameti, postgres nad tabulkou records). Vyber je na jednom
  miste v store/index.ts
- src/data/store/cached.ts: synchronni kopie v pameti pro entity ctene pri
  kazdem requestu (uzivatel v autorizaci, firmy pri vypoctu prav). Bez toho by
  se autorizacni middleware musel predelat na async
- src/routes/crud.ts: fabrika na CRUD routy. Kazda entita by jinak znamenala
  stejnych sto radku a sedmkrat by se opravila spatne
- src/data/bootstrap.ts: jedno misto, kde je seznam entit a jejich vychozi sady
- migrace 002_records.sql: jedna tabulka s JSONB. Tvary se jeste hybou a nikdo
  se nad nimi nedotazuje po polich. Az se to usadi, entita se povysi na vlastni
  tabulku, presne jako uz maji konektory

Prava a role (bod 5):
- role jsou zaznamy, pravo je retezec, katalog prav je zdroj pravdy. Union
  'admin' | 'agent' na ucetni a skladnika nestacil a pridavat hodnoty je slepa
  ulicka, kazdy klient chce jine
- Membership.roleIds misto role. Systemove role admin a agent zustavaji, takze
  se zadny ucet nemusel predelavat
- accessFor vraci prava i zalozky. Klient si nic nedovozuje

Zalozky a limity za firmu (bod 6):
- TenantFeatures: moduly, limity, zpristupnene sluzby. Dve vrstvy s jinym
  vlastnikem, ktere se nesmi michat: co ma firma zaplacene nastavujeme my,
  kdo z jejich lidi to smi nastavuje jejich admin. Efektivni viditelnost je
  prunik, takze vypnuty modul neexistuje ani pro admina te firmy
- navigace ze serveru, ne konstanta na klientovi

Firmy, uzivatele, resitele a skupiny: CRUD vcetne clenstvi a hesel. Heslo se
z API nikdy nevraci, ani jako hash. Skupiny resitelu kvuli tomu, ze prehazovat
praci na jmeno nestaci - clovek chce rict "tohle je pro ucetni".

Typy ticketu a akce (body 1, 2, 3):
- typ ticketu s vlastnimi polemi, plus tagy. Akce se vazou na typ nebo tag,
  ale za tagem nestoji zadna pole, takze akce na tagu umi jen vestavena pole
- Ticket dostal typeId, fields, tags a assigneeGroupId
- definice akce s telem jako operace, strom nebo skript. Pravo vznika spolu
  s akci jako action:<id>, admin pak zaskrtava akce, ne prava
- CTA na ticketu filtruje server podle typu, tagu, podminek a prav. Kdyby to
  pocital klient, pocitalo by se to na dvou mistech
- vestavene akce (typ, tagy, skupina) jdou pres tutez fabriku, takze maji
  svoje pravo a projdou auditem
- spusteni zapise do logu ticketu hned, jeste nez se neco stane

Vlastni widgety (bod 4):
- rozdeleni na render a source, groupBy, filtr je tentyz, ktery umi seznam
  ticketu. Uzivatel nepise dotazy
- jeden batch endpoint na cely prehled. Widget, ktery selze, vraci chybu na sve
  pozici a nezhasne prehled

Audit a impersonace (bod 6c):
- audit zapisuje i odepreni, jinak by pokusy o cizi firmu nikde nezustaly
- impersonace: jen spravce platformy, nikdy na jineho spravce platformy,
  30 minut bez obnoveni, vychozi jen cteni. Zapis pod rezimem cteni vraci 403
  na urovni middleware, ne az v handleru

Overeno bez databaze: vsech devet ulozist se nacte, role se zaloz1 a prezije
restart, agent dostane 403 na spravu roli a uzsi navigaci, neznama prava se
odmitnou, heslo se nevraci, typ a tagy ticketu se ulozi, CTA se objevi, widget
data pocitaji vcetne seskupeni, impersonace odmitne zapis i prepnuti na admina,
audit obsahuje actedBy. Degradace pri nedostupne databazi taky overena: migrace
selzou, jede se do souboru a rekne se proc.

Neovereno: migrace 002_records.sql proti zive databazi. Kontejner uz nebyl
k dispozici, generickou vrstvu drzi tentyz pool a migrator jako konektory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 07:06:38 +02:00
JiriUhlirandClaude Opus 5 6e3d0640ff Soubor jako uloziste, kdyz neni databaze
Mockup se k databazi nedostane, takze pribyl treti rezim: JSON soubor. Prezije
restart procesu i containeru, ale ne redeploy - filesystem containeru je
docasny. Je to mezistupen, ne nahrada databaze, a tak je to i napsane v portalu.

| Rezim    | Kdy                               | Restart | Redeploy |
| -------- | --------------------------------- | ------- | -------- |
| postgres | DATABASE_URL i SECRETS_KEY        | prezije | prezije  |
| file     | neni DB, ale je DATA_DIR          | prezije | ne       |
| memory   | ani jedno, nebo nejde zapsat      | ne      | ne       |

Rozhodnuti zustava na jednom miste (src/data/connectorStore.ts).

Pridano:
- src/data/snapshot.ts: atomicky zapis (.tmp a prejmenovani), slucovani zapisu
  a dokonceni rozepsaneho zapisu pri SIGTERM. Bez atomickeho zapisu by pad
  uprostred nechal polovicni JSON, ktery se pri startu nenacte. Rozbity soubor
  se prejmenuje na .broken a jede se dal - aplikace, ktera nenastartuje, je pro
  AppFactory nefunkcni sluzba
- src/data/connectors/local.ts: jeden kod pro pamet i soubor, lisi se jen tim,
  kam se zapisuje. Nahrazuje memory.ts, dve implementace by se casem rozesly
- klic k sifrovani se mimo databazi vygeneruje do DATA_DIR/secrets.key s pravy
  0600, takze sifrovani funguje bez nastaveni. Chrani to proti nahodnemu
  precteni JSONu, ne proti pristupu k disku - klic lezi vedle dat a je to tak
  napsane i v portalu. U databaze se negeneruje vubec: kdo ma zalohu tabulky,
  ma i klic ze stejneho stroje
- DATA_DIR v konfiguraci, data/ v .gitignore a .dockerignore
- hlaska v portalu rozlisuje tri nasledky: pamet, soubor a databaze

Overeno bez databaze: konektor s vyplnenymi udaji prezil restart, v JSONu jsou
hodnoty sifrovane a plaintext v nem neni. Pote s databazi: rezim postgres
funguje dal a klic vedle dat se nevygeneroval. Kontejner i data/ po overeni
smazany.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:18:05 +02:00
JiriUhlirandClaude Opus 5 61cf29878f Dockerfile: kopirovat migrace do image
tsc do dist kopiruje jen .js, takze .sql migrace by v containeru chybely.
Aplikace by spadla na migracich a jela dal v pameti, tedy presne to, co ma
databaze resit. Cesta odpovida dist/db/migrations, odkud si je hleda
src/db/migrate.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:09:21 +02:00
JiriUhlirandClaude Opus 5 78e7f99d60 Konektory do Postgresu, pristupove udaje sifrovane
Pristupove udaje konektoru se ukladaji do databaze a prezijou restart. Popis
v documentation/14-databaze.md.

Databaze je volitelna a rezimy jsou oddelene:
- postgres kdyz je DATABASE_URL i SECRETS_KEY
- memory jinak, tedy pri nasazenem mockupu a lokalnim vyvoji bez DB

Rozhodnuti je jen na jednom miste (src/data/connectorStore.ts). Nikde jinde se
nezjistuje, jestli databaze je - kdyby se to rozlezlo po kodu, jedno misto by se
zapomnelo a chovalo by se pak jinak nez zbytek.

Chybejici databaze nesmi shodit start: container, ktery nenastartuje, je pro
AppFactory nefunkcni sluzba. Misto toho se do logu napise proc a portal to ukaze
na strance Konektory. Stejne tak kdyz migrace selzou - psat do rozbiteho
schematu je horsi nez neukladat.

Databaze potrebuje oboji. Bez SECRETS_KEY by se udaje ukladaly v plaintextu
a to je horsi nez ztratit je pri restartu: tabulku vidi kazda zaloha a kazdy
dump pri ladeni.

Pridano:
- pool v src/db/pool.ts vcetne transakci a dbFor(tenantId) jako sev pro budouci
  oddelenou databazi jednoho klienta
- migrace ze src/db/migrations/*.sql pod pg_advisory_lock, jinak je pri rolling
  deployi pusti vsechny instance naraz. Jeden soubor je jedna transakce
- sifrovani AES-256-GCM s nahodnym IV a verzi klice. Nerozsifrovatelna hodnota
  nepada, chova se jako nevyplnena a zaloguje se - jeden rozbity konektor nesmi
  shodit seznam ostatnich
- /health/ready s pingem do DB. /health na databazi zamerne nezavisi, kratky
  vypadek by jinak vedl k restartovani containeru
- GET /api/dashboard/storage a hlaska v portalu o tom, ze data jsou jen v pameti
- jediny vychozi konektor na firmu a sluzbu hlida castecny unikatni index, ne jen
  kod. Dva soubezne zapisy by jinak udelaly dva vychozi

Zmeneno: cteni i zapis konektoru je asynchronni, vcetne validace stromu.

Overeno proti Postgresu 16 v kontejneru: migrace, sifrovani v tabulce, preziti
restartu, rozsifrovani spravnym klicem, degradace pri spatnem klici, PATCH bez
tajneho pole, prepnuti a smazani vychoziho konektoru, pametovy rezim bez
DATABASE_URL. Kontejner po overeni smazan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:08:25 +02:00
JiriUhlirandClaude Opus 5 3279dd7dac Cele chybove hlaseni u konektoru i skriptu
U odpovedi 401 nebo 400 je duvod napsany v tele odpovedi sluzby, ne v tom, ze
prislo 401. Dosud se telo zkracovalo na 400 znaku a u overeni konektoru se
zahazovalo cele - zbyla veta "Pristup zamitnut", podle ktere se neda hledat.

- ScriptError nese `request` (metoda a cesta) a `detail` s celou odpovedi
  sluzby, zkracenou az na SCRIPT_ERROR_DETAIL_BYTES (vychozi 8 kB). Chyby jsou
  vzacne, takze objem neroste jako u logu uspesnych kroku
- do detailu jde surove telo, ne prochazene pres JSON.stringify. U chyby chceme
  presne to, co sluzba poslala, vcetne HTML nebo prosteho textu
- u chyby spojeni se pridava i `cause`, u neocekavane vyjimky zasobnik volani
  (mimo produkci, stejne jako u centralniho error handleru)
- overeni konektoru vraci `detail`, `status` i `request`
- cely detail jde i do logu serveru, at je to dohledatelne bez portalu
- do chyby se dava jen cesta, ne cela adresa: v query muze byt tajemstvi
- nova komponenta ErrorDetail: rozbaleni cele odpovedi a tlacitko Kopirovat vse

Overeno: npm run typecheck prochazi na serveru i webu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:38:43 +02:00
JiriUhlirandClaude Opus 5 ad56c7f513 Transformace dat, oprava ukladani udaju konektoru
Transformace dat ve dvou rezimech plus oprava chyby, kvuli ktere se neukladaly
pristupove udaje konektoru. Popis v documentation/13-transformace-dat.md.

Kroky si predavaji i cele struktury:
- FieldType ma object a list. Do sablony se nedosazuji, predavaji se jako celek
  dalsimu kroku - proto je u nich v builderu vyber a ne textove pole. Z podminek
  nad nimi ma smysl jen "prisla / neprisla"
- strop na velikost struktury (SCRIPT_MAX_VALUE_BYTES, vychozi 256 kB). Radek
  s vystupem kroku je nejrychleji rostouci tabulka v systemu

Dva rezimy transformace, oba nad enginem v src/scripts/mapping.ts:
- transform.map-fields: pole na pole s prevody, klikatelne
- transform.to-json: sablona cileveho objektu s ${cesta}

Marker ${...} je zamerne jiny nez {{...}}. Sablony kroku se dosazuji driv, nez
krok bezi, takze {{total}} by strom stihl vyhodnotit, nenasel by parametr toho
jmena a dosadil by prazdno. Cely retezec navic zachova typ, takze
"unitPrice": "${total}" vyrobi cislo - jinak by cizi sluzba dostala castku jako
text a odmitla ji.

Prevod map pro seznamy je to, bez ceho by priklad nesel dokoncit. Bez nej jde
prevest hlavicku dokladu, ale ne polozky objednavky, a doklad by byl na nulu.

Dal pridano:
- idoklad.create-invoice-from-object: druha polovina prikladu, bere hotove telo
  dokladu z transformace a doplni povinna pole ze vzoru iDokladu
- spoustec e-shopu predava celou objednavku jako objekt a polozky jako seznam
- klikaci editor pravidel vcetne rezimu JSON pro vnorena pravidla u map
- kontrola JSONu a tvaru pravidel uz pri ulozeni stromu. Preklep je nedodelek,
  ne chyba ukladani - rozdelana prace se nezahazuje

Opraveno: konektor neukladal pristupove udaje. Server byl v poradku, overeno
volanim POST i PATCH. Chyba byla v prohlizeci: u pole type="password" prohlizec
ignoruje autocomplete="off" a dosazuje ulozene prihlaseni. Uzivatel pak videl
jednu hodnotu, React drzel jinou, a ulozilo se to, co drzel React, tedy nic.
Resi to autocomplete="new-password", jmena poli, ktera nepripominaji heslo,
a prepinac zobrazeni, aby slo overit, co je opravdu zapsane.

Zakladani a uprava konektoru se presunuly do dialogu, na strance jsou jen male
karty. Formulare rozlozene po strance byly u vic konektoru neprehledne.

Overeno: npm run typecheck prochazi na serveru i webu, node --check na skriptech.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:32:39 +02:00
JiriUhlirandClaude Opus 5 8ad91a6c28 Rozdeleni na sluzby a konektory, pristupove udaje do konektoru
Slovo "konektor" v kodu znamenalo katalog toho, co umime. Ted znamena napojeni
jedne firmy, tedy to, co tim mysli i uzivatel. Popis modelu je
v documentation/12-sluzby-a-konektory.md.

Tri vrstvy:
- Sluzba: ze iDoklad existuje, co umi a co potrebuje k napojeni. Nase.
- Skript: kod, ktery jednu operaci sluzby opravdu vykona. Nas.
- Konektor: ucet firmy vcetne jejich pristupovych udaju. Firemni.

Pristupove udaje se prestaly cist z environment variables. Cela instance by
mela jedny udaje spolecne a dve firmy by fakturovaly z jednoho uctu. Napojeni
je vlastnost firmy, ne prostredi. Z prostredi zustava jen SERVICES_BASE_URL.

Pridano:
- src/data/services.ts: sluzba nese general, appId, visibility, credentials
  a verifyPath. Kategorie "obecne" sdruzuje veci, ktere ma kazdy a nepotrebuji
  konektor: webhook, planovac, tickety, transformace dat, HTTP pozadavek,
  pauza, zapis do logu
- viditelnost sluzby: vsichni, jen uvedene firmy a lide, nebo jen spravce
  platformy. Neviditelna sluzba se z API nevraci vubec, ne se stavem 403 -
  firma nema poznat, ze takova sluzba existuje
- src/data/connectorStore.ts: konektory za firmu vcetne hodnot udaju. Hodnoty
  se z API nikdy nevraci, jen filled a missing. Prazdne pole hodnotu nemeni,
  takze ulozeni formularu bez tajnych hodnot nic nepresepe
- FlowStep.connectorId: krok rika, pod kterym napojenim volat. null = vychozi
  konektor firmy, diky tomu je vzorovy strom prenositelny mezi firmami
- overeni konektoru pres verifyPath, tedy cteci volani vyzadujici autorizaci.
  U sluzby bez nej se overi jen dostupnost a odpoved to rekne nahlas, jinak by
  zeleny vysledek uzivateli lhal
- stranky /dashboard/sluzby a /dashboard/konektory vcetne formularu udaju
- endpointy /api/dashboard/services a CRUD /api/dashboard/connectors ve Swaggeru
- predvyplnene prihlaseni spravcem platformy a prepinac demo uctu na login
  strance, kvuli testovani prototypu

Zmeneno:
- stav "napojeno" se prestal cist z katalogu a zacal pocitat z konektoru firmy.
  Sluzba ma jen available nebo planned
- validace stromu overuje i konektor. Cizi konektor je chyba, chybejici
  napojeni nedodelek - rozdelana prace se nezahazuje
- prejmenovani napric kodem: Connector na Service, FlowStep.connectorId na
  serviceId, GET /connectors na GET /services, connectorIcons na serviceIcons,
  stranka Konektory (katalog) na Sluzby. Prevodni tabulka je v dokumentu 12

Overeno: npm run typecheck prochazi na serveru i webu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 14:11:50 +02:00
JiriUhlirandClaude Opus 5 6f6b287d7e Skripty konektoru: vykonna cast s manifestem a kontrolou parametru
Konektory dostaly vykonnou cast. Jeden skript je jeden soubor, ktery nese
manifest (vstupni a vystupni parametry) i kod. Diky manifestu s nim umi
pracovat strom automatizace, aniz by o kodu cokoliv vedel.

Soubory jsou zamerne obycejny JavaScript, ne TypeScript. TypeScript by se
musel prelozit a to je presne to otaceni, ktere tady nema byt. Registr
sleduje cas zmeny souboru, takze uprava v portalu, rucni uprava souboru
i novy soubor ve slozce funguji stejne a bez restartu.

Pridano:
- scripts/ se skripty konektoru, nazev souboru je zaroven ID operace
- kontrola vstupu i vystupu proti manifestu, jedna funkce pro obe strany.
  Chybejici povinny vystup je chyba skriptu, ne uzivatele - jinak by strom
  veril parametru, ktery nikdy nedosel
- ctx predavany skriptu: http nad adresou napojeni, util, log, config,
  idempotencyKey, fail a retry. Skript nedostane pristupove udaje
- rozliseni opakovatelne a koncove chyby. Runner nikdy nevyhodi vyjimku,
  vzdy vraci vysledek vcetne retryable
- redakce tajnych hodnot pred zapisem do logu. Cizi API rado vraci prijaty
  token v chybove zprave a log ticketu vidi klient
- napojeni z environment variables vcetne iDokladu
- sest ukazkovych skriptu pro iDoklad proti skutecnemu API sluzby
  services.csbot.cz/apps/idoklad, kazdy na jiny vzor
- stranka /dashboard/skripty: seznam, manifest, editor, zkusebni spusteni.
  Formular testu se sklada z manifestu, nepise se pro kazdy skript
- endpointy /api/dashboard/scripts vcetne Swaggeru

Zmeneno:
- katalog konektoru uz neni jen staticky seznam. Akce ze skriptu se domeruji
  prekryvem v src/data/connectors.ts, takze se naraz objevi ve validaci
  stromu, ve vypoctu scope i v sablonach. Pri stejnem ID vyhrava skript
- ConnectorOperation ma implementation a scriptId
- ApiError na klientovi nese cele telo odpovedi a umi z nej vytahnout issues
- Dockerfile kopiruje scripts/ do vysledneho image

Ukladani nemuze rozbit fungujici skript: kod se nejdriv zapise do docasneho
souboru, ten se nacte a overi, a az pak prepise puvodni.

K tomu tri dokumenty navrhu dalsich kroku: 09 datove modely a prava,
10 runtime a rozpocet na 150 klientu, 11 popis skriptu konektoru.

Overeno: npm run typecheck prochazi na serveru i webu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 13:37:58 +02:00
JiriUhlir bbc2236c0d dashboard widgets 2026-08-03 13:10:41 +02:00
JiriUhlirandClaude Opus 5 2a3d75c85e Firmy a prava: tenance napric portalem
Portal nemel zadnou tenanci. Kterykoliv prihlaseny uzivatel videl vsechny
tickety vsech firem i cely seznam resitelu, requireRole se nikde nevolal.

Tenant je hranice viditelnosti, tenantId na ticketu, resiteli i automatizaci.
Uzivatel muze patrit do vic firem, v kazde s jinou roli. Pristup napric firmami
je zvlast jako platformAdmin.

Tri pohledy na tickety: all, tenant, mine. Admin mezi nimi prepina vcetne
vyberu firmy. O pravech rozhoduje jedine data/access.ts, klient si nic
nedovozuje a bere je z GET /api/dashboard/access.

Filtr na firmu je v ulozistich povinny argument, takze zapomenuty filtr
neznamena vse, ale nezkompiluje se. Cizi firma vraci 403 nebo 404, nikdy
tise zuzeny vysledek.

Prirazeni jen v ramci firmy. Prehazovat praci mezi lidmi smi jen admin,
agent si smi vzit ticket na sebe.

Zmena prihlasovani: ucet klient@firma.cz zanikl, demo ucty jsou nove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:59:30 +02:00
JiriUhlirandClaude Opus 5 dd021b5f69 Obsah ticketu, nastaveni kroku a vystupy kroku
Ticket dostal telo (body) a odkaz na zdrojovou zpravu. Predmet je shrnuti,
telo je cely text pozadavku.

Akce maji nastavitelna pole (inputs) se sablonami {{parametr}}. Zatim ticket,
kanaly, CRM a AI, ostatni maji jen napovedu.

Krok vidi parametry spoustece plus vystupy kroku pred nim, takze jde vlozit
predvalidaci a vetvit se podle jejiho vysledku. Vetev podminky nepridava nic
do sekvence za podminkou.

Nove konektory Facebook Messenger a Instagram, nova akce RAYNET Dohledat firmu.

Ctyri vzorove automatizace v rozdeleni jedna na kanal pro prijem
a jedna spolecna pro smerovani na resitele.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 12:29:26 +02:00
JiriUhlir 52b190bfbc updated tickets 2026-08-03 11:45:35 +02:00
JiriUhlir 9a0c67af08 Odstraneni docasnych souboru z testovaciho behu 2026-07-31 17:05:01 +02:00
JiriUhlir e177ca2087 Oprava: aplikace padala pri startu v produkcnim rezimu
Dockerfile nastavuje NODE_ENV=production a config.ts pri chybejicim
JWT_SECRET vyhazoval vyjimku. AppFactory tu promennou nastavenou nema,
takze se container po startu ukoncil a sluzba byla nedostupna (502).

Chybejici JWT_SECRET uz start neshodi. Vygeneruje se nahodny klic platny
do restartu containeru a do logu jde varovani. Klic zapsany v kodu se
nepouziva, aby nevznikla bezpecnostni dira.

Dusledek: bez nastavene promenne prestanou po restartu platit vydane
tokeny. JWT_SECRET se ma nastavit jako promenna aplikace v AppFactory.
2026-07-31 17:04:32 +02:00
JiriUhlir 7b045a9f20 Nahrazeni sablony kompletnim webem a klientskym portalem
Web a portal Automia v jednom containeru. Express obsluhuje API
i zbuildovanou React aplikaci z dist/public.

Obsah:
- verejny web: homepage, sluzby, o nas, kontakt, 404
- prihlaseni pres JWT, demo ucty
- portal: prehled s grafem, tickety, incidenty, automatizace, konektory
- builder automatizaci: strom akci, vetveni podminkou
- katalog 25 konektoru v 8 kategoriich
- webhook s registrovanou adresou, token generuje server
- zivy dashboard pres SSE vcetne simulace provozu
- Swagger UI na /docs a OpenAPI na /openapi.json

Soulad s AGENTS.md:
- ROOT_PATH z prostredi, prefix proxy nikde nehardcodovan
- mount na koren i na prefix, funguje s handle_path i bez nej
- base tag a window.__BASE_PATH__ vkladane do index.html za behu
- OpenAPI servers obsahuje prefix, Try it out vola spravnou adresu
- povinne /health a /docs, port 3000, naslouchani na 0.0.0.0
- secrets jen z environment variables, nikdy v logu

Dokumentace ve slozce documentation/.
2026-07-31 17:00:37 +02:00
AppFactory Bot 46f2f0b07e Initial Node.js TypeScript service 2026-07-31 14:42:26 +00:00