Commit Graph
72 Commits
Author SHA1 Message Date
JiriUhlirandClaude Fable 5.1 104ae36783 Revize projektu: prava, vykon, runtime, portal a ARES
Prava a bezpecnost: spravce firmy uz nemuze nastavit priznak spravce
platformy ani clenstvi v cizi firme; pozvanky, konektory a automatizace
kontroluji sve pravo; cizi firma v query je 404; zivy stream posila
udalosti jen firmam, kterych se tykaji; akce nad ticketem maji kontrolu
prava za firmu ticketu a strop viditelnosti; tokeny se nelogujou; limit
pokusu na prihlaseni, kontakt a pozvanky; bezpecnostni hlavicky;
zachyceni chyb v async handlerech; timing-safe porovnani tokenu.

Vykon: audit neskenuje celou kolekci pri kazdem zapisu a konecne maze
firemni zaznamy; ticket se uklada jednou misto trikrat; zapisy do
Postgresu jsou serializovane podle ID; prava se pocitaji jednou na
request; widgety nacitaji tickety jednou; strankovani seznamu; worker
je pool misto kol; na webu udalost ze streamu neodmontuje stranku,
dotazy maji spolecny debounce a cache, ciselniky drzi typovany sklad.

Runtime: opakuji se jen chyby oznacene retryable; smycka nenarazi na
strop 50 kroku (novy strop 1000 akci); podminka nad datem funguje;
vystup MCP nastroje neprepisuje spoustec; sandbox skriptu firmy nejde
opustit; MCP session id se drzi mezi volanimi; incident z kroku patri
firme; jedno rozhodnuti o rezimu uloziste; snapshot neprepise soubor
po chybe cteni.

Refaktory: sdilene typy API v src/shared (web nic nekopiruje, osm
rozjetych tvaru sjednoceno); spolecny modul net/guard pro volani ven;
formularova vrstva ui/form; rozdeleni Connectors a FlowCanvas; jeden
helper pro firmu z query, validaci a CRUD udalosti; pomucky ctx.util
pro skripty konektoru; i18n verejneho webu vcetne anglictiny.

Nova funkce: zalozeni firmy z registru ARES v Nastaveni (IC nebo nazev,
dotazeni IC, DIC, sidla a pravni formy, vyber soucasnych statutarnich
zastupcu a prokury, ucty spravce firmy s nahradnim e-mailem
IC-poradi@placeholder.cz).

Dokumentace: zaznam v 99-zmeny.md a aktualizace 15 dalsich dokumentu.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 10:26:07 +02:00
JiriUhlirandClaude Opus 5 0c405ea55a Otevrena stranka sekala prehravani videa
Pri otevrenem portalu zacalo vedle nej sekat prehravani videa, po zavreni 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. Byly to
tri veci.

Rozmazavani plne barvy. `.glass`, 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
na peti mistech, kde pod nim neco opravdu projizdi: lepici hlavicky, podklad
modalu, prekryv menu na mobilu.

Nekonecne animace nad velkymi rozmazanymi plochami. Prihlaseni a verejny web
mely zare o velikosti 26 az 34 rem s blur(120px), kterym se donekonecna
animovala pruhlednost - to znamena prepocitavat obrovskou vrstvu sedesatkrat za
sekundu, i kdyz se na strance nic nedeje. Zare zustaly, animace zmizela.
Pribylo prefers-reduced-motion.

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 - tedy vsechny dotazy s refetchOn na strance, i kdyz o ten
typ udalosti nestaly. Pri behu automatizace se cely dashboard prekresloval
nekolikrat za sekundu, a s nim dvacet rozmazanych vrstev. Kontext je proto
rozdeleny: useEventStream da stav a odber, useEventLog seznam udalosti.

Filtr sekci nefiltroval. Seznam ticketu sklada adresu dotazu v useMemo, ale
v zavislostech chybely groupId, typeId, tag a stage - adresa se po prepnuti
sekce neprepocitala a data se nenacetla znovu. Driv to nevadilo, byly to filtry
nastavitelne jen odkazem, ktere se po nacteni nemenily. Se zalozkou "Vsechny
tickety" a jejim vyberem sekce je to chyba, ktera je videt.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 15:23:43 +02:00
JiriUhlirandClaude Opus 5 9f4b8c0596 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.

Model: rules (otazky) a match ('all' = a zaroven, 'any' = 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.

fieldId je v typu nepovinne schvalne: prekladac tim ukazal vsech pet mist,
ktera podminku ctou (vyhodnoceni, obe hlasky do logu, kontrola pri ukladani,
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 odkaz na parametr pres
ID - diky nemu prejmenovani parametru podminku nerozbije a builder umi nabidnout
jen operatory, ktere na dany typ sedi. Az se ukaze, ze jedna uroven nestaci,
da se textovy zapis pridat nad tentyz vyhodnocovac. Opacne to nejde.

V builderu radek na otazku, tlacitko "Přidat otázku" a od druhe otazky prepinac
"sedí všechny" / "sedí aspoň jedna". Posledni otazka nejde smazat: podminka bez
otazky by tise nevetvila, runtime ji povazuje za nesplnenou a zaloguje to.

Radek podminky v logu nese vsechny otazky i s tim, ktera rozhodla - bez rozpadu
by u spojene podminky bylo videt jen "nesplneno".

Overeno na bezici instanci, strom se dvema spojenymi podminkami a peti volanimi:
vysledek nedorazil (nic), Chybějící informace (prirazeno), Přesměrování
a Mimo téma (zavreno pres any), Něco jiného (nic). Strom ulozeny ve stare
podobe se nacte a ulozi beze zmeny.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 09:16:19 +02:00
JiriUhlirandClaude Opus 5 1532ea0af7 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.

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.

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.

Opraveny strom se nejdriv pta, 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.

Radek podminky v logu rikal jen "splneno". Ted nese i to, s cim se porovnavalo,
a rozlisuje nedorazilo od prazdneho - prvni radek je presne ta informace, ktera
chybela:

  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"

Overeno prehranim celeho hovoru na bezici instanci: prvni zprava bez dat ticket
zalozi a nechá otevreny, druha s "Chybějící informace" ho preda a priradi, treti
s "Přesměrování" ho zavre. Pred opravou byl vyrizeny uz po prvni zprave.

Zustava k rozhodnuti: pri prehodnoceni hovoru resitel na zavrenem ticketu
zustane. Do "Mych ticketu" nespadne, ty ukazuji jen nevyrizene, ale ve vsech
ticketech u nej to jmeno stoji.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 09:01:09 +02:00
JiriUhlirandClaude Opus 5 f7ad429b3a 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:

- server cte BRAND_NAME za behu, vychozi WorkNuke
- klient cte VITE_BRAND_NAME pri buildu, vychozi tataz

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 15:02:39 +02:00
JiriUhlirandClaude Opus 5 5984b1cb9b Oprava: "Interni chyba serveru" pri zalozeni ticketu
Formular noveho ticketu koncil na 500 uz pri vyplnenem samotnem predmetu.

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. Tataz chyba byla i ve dvou mistech helpdesku,
u zalozeni pozadavku a u komentare.

Druha polovina je hlaska. Vyjimka z parseru tela propadla do centralniho error
handleru, ktery z ni udelal 500 "Interni chyba serveru" - hlasku, ktera rika,
ze je neco spatne u nas, a posila cloveka hledat na spatnou stranu, pritom slo
o spatne polozeny dotaz. Error handler proto chyby parseru tela rozpozna
(status 400 a type zacinajici entity.) a vraci 400 s vetou o neplatnem JSONu.
Ostatni chyby zustavaji 500, jak byly.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 14:59:02 +02:00
JiriUhlirandClaude Opus 5 79c9a729f6 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.

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 log. Priznak general uz existoval,
jen ho nikdo nepouzil na filtrovani nabidky.

Katalog se kvuli tomu nefiltruje, jen se oznacuje: GET /api/dashboard/services
prida ke kazde sluzbe connected. Sluzba z odpovedi nemizi, protoze log ticketu
a detail akce podle katalogu prekladaji ID operaci na jmena - kdyby zmizela,
zustalo by v uz zapsanem radku hole ID. Filtruje az builder. Pod nabidkou je
veta, kolik sluzeb ceka na napojeni, aby to nevypadalo, ze neexistuji.

Soukroma sluzba uz neni videt cizi firme. canSeeService vracelo spravci
platformy true driv, nez se vubec podivalo na viditelnost, takze zakazkova
integrace omezena na jednoho klienta se ukazovala i po prepnuti do jine firmy
a spravce si ji mohl vybrat do jeji automatizace. Nove rozhoduje firma, ne
clovek: kdyz je vybrana, plati jeji seznam, bez ni 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 na bezici instanci: 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.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 13:04:54 +02:00
JiriUhlirandClaude Opus 5 e701bc0e2c 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:

- Moje tickety, pro toho, kdo je veden jako resitel. Bez sloupce Resi, byl by
  tam porad on
- Nezarazene, pro kazdeho. Radky s rychlym prirazenim
- Vsechny tickety, jen pro toho, kdo vidi i cizi. 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. 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.

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. Kdo nesmi rozdavat praci ostatnim, vidi jen Vzit si. Endpointy uz
existovaly, nova je jen cesta k nim.

Vychozi zalozka se odvodi z 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.

Do navrhu 25 zapsano, ze hledani ma zapadnout do zalozky, ve ktere clovek stoji,
ne ji obejit, a vysledek se ma vracet do te same zalozky.

Overeno na bezici instanci: spravce platformy ma zalozku Vsechny a vsechny sekce
ve filtru, Vomacka jako vedouci Servicedesku jen svoji sekci, Kriz jako radovy
clen zalozku Vsechny nema. 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.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:48:15 +02:00
JiriUhlirandClaude Opus 5 52999a961b 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 bez resitele a bez skupiny. 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. 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: zaznam v katalogu a zdroj
v builtinSources, zadna nova komponenta, data pocita tataz cesta jako u vykonu
resitelu.

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

Vychozi rozlozeni ma nahore to, co clovek muze udelat, teprve pod tim cisla.
Graf behu z vychozi sady ven, je to nejmene srozumitelna dlazdice pro noveho
cloveka a zabira celou sirku. V katalogu zustava. Zmena se projevi jen tem, kdo
si dashboard jeste neupravili.

Overeno na bezici instanci: Kriz jako radovy clen sekce vidi ve svych ticketech
TK-4820, ve fronte nezarazeny TK-4819 a v seznamu oba. Pred opravou personId
vracela prvni dlazdice prazdno.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:37:55 +02:00
JiriUhlirandClaude Opus 5 2a310137a2 Detail ticketu ma rozradovace, ne tri karty pod sebou
Navrh v documentation/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:

- Pozadavek - obsah a pole na komentar, vychozi
- Log prubehu - co delala automatizace a co ji sluzby vratily
- Udalosti - co presne prislo zvenku
- Vlastni 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.

Overeno na bezici instanci: ticket z automatizace ma 5 kroku logu, 2 udalosti
a zadna vlastni pole, takze se ctvrty rozradovac skryje.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:29:18 +02:00
JiriUhlirandClaude Opus 5 6bcd7f845c 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:

- 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. Priznak visi na clenstvi, ne na cloveku a ne na skupine -
  diky tomu muze byt clovek v peti sekcich a jen ve dvou videt vsechno, coz
  role rict neumi, ta je jedna na celou firmu
- stara podoba personIds se dal cte, prevadi ji groupMembers

Vypocet stropu (visibilityFor), sjednoceni ne prunik: spravce platformy nebo
seesAllTenant vidi celou firmu, jinak svoje tickety plus vse ze sekci, kde ma
zaskrtnuto, plus jejich fronta. U zaznamu bez priznaku rozhoduje pravo
ticket.assign.others - kdo smel prehazovat cizi praci, uz stejne cely provoz
videl, takze se mu nic nebere.

Vynuceni:

- strop je povinna soucast TicketFilter, stejne jako tenantIds. Nepovinny filtr
  na prava je filtr, ktery jednou nekde chybi - takhle prekladac ukazal vsech
  trinact mist, ktera ho jeste nemela
- 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
- detail a prevzeti pocitaji strop za firmu ticketu, ne za prave prepnutou

Helpdesk se ridi toutez hierarchii, jen "moje" znamena, co jsem zalozil -
zadavatel pozadavek nikdy nema prirazeny. Ticket proto nese createdById.

Zalozka Firma: pod jednim mistem Prehled, Resitele, Sekce, Role a prava, Typy
ticketu a Pozvanky. V Nastaveni zustal Muj ucet a platformni veci. Role a Typy
jsou samostatne komponenty, sekce maji vlastni panel - u kazdeho clenstvi je
prepinac a radek na cloveka obecny EntityAdmin neumi.

Obsah ticketu se cte i v helpdesku, v seznamu jako jednoradkovy nahled.

Overeno na bezici instanci se tremi ucty nad tymiz daty: spravce platformy vidi
vsechny tri tickety vcetne neprirazeneho, Vomacka jako vedouci Servicedesku dva
(tickety obou clenu sekce), Kriz jako radovy clen jeden, jen svuj. Adresa cizho
ticketu vraci 404, vytizeni tymu ukazuje jen viditelne a po zaskrtnuti priznaku
se rozsah zmeni hned.

Co z toho plyne: neprirazeny ticket bez skupiny nevidi nikdo krome toho, kdo
vidi celou firmu. Prichozi praci musi nekdo smerovat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 12:15:47 +02:00
JiriUhlirandClaude Opus 5 a6722f9379 Obsah ticketu se na detailu cte, ne jen vypisuje
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е:

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

Vedle toho zdroje vizualniho navrhu detailu ticketu v design/. Sestavene platno
je v gitignore, je to 2,5 MB generovaneho souboru, ktery vznikne znovu ze
zdroju vedle nej.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 11:41:18 +02:00
JiriUhlirandClaude Opus 5 40875439e4 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 - a zaplatilo za to celym uzitkem
te funkce.

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

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

Overeno na bezici instanci s vlastnim DATA_DIR, telem s data jako textem misto
objektu a bez povinneho status:

  ok     callSid  | cekame string, povinny | prislo text = CA-B
  CHYBA  status   | cekame string, povinny | prislo neprislo
  CHYBA  data     | cekame object          | prislo text = tohle je text misto objektu

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 10:46:36 +02:00
JiriUhlirandClaude Opus 5 e8bb4d6e54 Kontrakt webhooku: objekt jde vybrat a odmitnuti je videt
Parametr spoustece `data` byl deklarovany jako type string a povinny, 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
- 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. Incident se z toho nezaklada
a upozorneni se neposila: staci radek, implementator se ozve sam.

Vzorova automatizace ma data opravene na object a nepovinne.

Do navrhu 25 jsou zapsana rozhodnuti z diskuze: "moje tickety" jsou tickety
prirazene mne, v helpdesku ty, ktere jsem zalozil ja, a helpdeskove pozadavky
vidi lide podle teze hierarchie jako tickety. Sekce 6 popisuje tuhle zmenu.

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.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 10:14:48 +02:00
JiriUhlirandClaude Opus 5 72da07debe Navrh pristupneho portalu a srovnani vzorove automatizace s instanci
Dve veci, obe bez zmeny chovani aplikace.

Navrh (documentation/25-navrh-pristupny-portal.md):

Vzniklo 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". Prvni dva jsou skoro zadarmo,
  builtinSources uz ten mechanismus maji
- formulare nemaji spolecnou vrstvu. inputClass je nadefinovany na 13 mistech
  a rozesel se do peti ruznych vzhledu, Field je napsany trikrat. V ui/ neni
  zadny formularovy prvek. Blokuje to widget akci, protoze NewTicketDialog je
  ten modal a formular z nej vytahnout nejde
- 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, coz podpira bod o hledani

Dve veci, ktere stoji za zapamatovani, i kdyby se navrh nikdy nedodelal:
strop viditelnosti nepatri do hledani, ale do cteni ticketu (cesty k ticketum
jsou tri a dve z nich filtruji tickets primo), a pohled a strop nejsou totez.

Ctyri otevrene otazky jsou v zaveru navrhu.

Vzorova automatizace (src/data/automationStore.ts):

seedRealAutomations drzelo starsi podobu stromu nez ta, ktera na instanci
opravdu bezi. Protoze data neprezivaji redeploy, je tenhle seed jedine misto,
kde nastaveni prezije nasazeni - 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), krok upsert 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.

Overeno lokalnim startem s vlastnim DATA_DIR: automatizace se nasype, ma ctyri
kroky stejne jako instance, je zapnuta a nema zadny nedodelek.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 09:37:46 +02:00
JiriUhlirandClaude Opus 5 1134852bff Zalozit nebo doplnit ticket: doplneni konecne doplnuje
Krok mel v poli Obsah nastaveno {{rating}}. Data v behu prokazatelne byla,
v udalostech ticketu je hodnoceni videt cele, ale ticket zustal s prazdnym
obsahem.

intakeEvent deli praci na zalozeni a navazani na existujici ticket a vsechno
z `create` platilo jen pro tu prvni vetev. U existujiciho ticketu se doplnovaly
pouze vlastni pole a stitky, zbytek se tise zahodil. U hovoru to znamena, ze
obsah nedorazi nikdy: prvni zprava jen oznami, ze hovor zacal (in-progress,
data null), a prave ta ticket zaklada. 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.

Jedno pravidlo misto dvou seznamu poli:
- neprazdna hodnota prepise, prazdna nemaze. IntakeInput ma na to `apply`,
  v `create` zustala jen zaloha predmetu a vychozi stav
- 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
- vyjimky zustavaji dve: 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
- prazdny retezec 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, coz uz je zamer

Dalsi dve veci, ktere u toho vyplavaly:
- 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 nem nikdy nebyl
- faze byla zrusena uz driv, ale v katalogu po ni zbyval krok "Posunout do
  dalsi faze" a pole Faze u zalozeni ticketu. Ticket ani typ ticketu fazi
  nemaji, takze krok by selhal na chybejicim skriptu a pole se zahazovalo.
  Oboji je pryc

Krok navic v logu rekne, co doplnil: "doplnen TK-123, stav completed, obsah".
Driv radek jen oznamil, ze se ticket doplnil, a nebylo poznat cim.

Overeno na bezici instanci s vlastnim DATA_DIR, tremi zpravami o jednom hovoru:
prvni zaklada ticket s prazdnym obsahem, druha doplni obsah i zakaznika, treti
bez dat je nechava byt a meni jen stav. Scenar s `data` a pak `data2` ma na konci
obe hodnoty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 08:09:05 +02:00
JiriUhlirandClaude Opus 5 35d1e43307 Pad vykreslovani uz nesmi shodit stranku a sam zaklada incident
Ukazka tela webhooku shazovala 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, takze totez mohlo prijit odkudkoli.

Pojistka:
- 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. Odchod jinam pojistku srovna zpatky
- je to jedina trida v celem klientovi, React to jinak zachytit neumi

Incident:
- POST /api/dashboard/client-crash zaklada incident z padu. Bez toho je jedina
  stopa v konzoli prohlizece uzivatele, kam se nikdo nedostane
- title a impact cte zakaznik, 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

Sama ukazka:
- parametr, kterym vede cesta jineho parametru, uz nedostane zastupnou hodnotu
  podle typu. Kdyz je jeden parametr `data` a druhy `data.result`, `data` musi
  byt objekt a zastupna hodnota by ho prepsala
- cela ukazka je v try. Je to napoveda a nesmi shodit ani ten kus obrazovky

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 11:37:19 +02:00
JiriUhlirandClaude Opus 5 00d9d2d4b1 Ukazka tela webhooku shazovala builder pri psani cesty parametru
Zapisovalo se do retezce: `exampleValue` vraci u obycejneho parametru text
'text', a kdyz cesta jineho parametru vedla skrz nej, `target[key] = ...`
skoncilo na "Cannot create property 'data' on string 'text'" a spadl cely
builder.

Neni to okrajovy stav. Prijde cely objekt a vedle nej se vytahuje konkretni
hodnota z jeho nitra, takze parametr, kterym vede cesta jineho parametru, je
bezna vec - a pri psani cesty `zprava.data` je `zprava` chvili hotovy parametr
sam o sobe, takze to padalo pri kazdem stisku klavesy.

Dve zmeny:
- do cesty se da zanorit vzdy. Kdyz na miste je skalar, nahradi se schrankou
- parametr, kterym vede cesta jineho parametru, uz nedostane zastupnou hodnotu
  podle typu. Plati vnitrni struktura, protoze prave tu ma ukazka ukazat -
  zastupna hodnota by ji prepsala, a u objektu a poli je to to podstatne

Overeno na obou poradich (skalar driv i pozdeji nez hlubsi cesta), na objektu
s vnitrkem a na poli s ciselnym indexem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 11:25:08 +02:00
JiriUhlirandClaude Opus 5 4d156d2837 MCP EasyWeb podle skutecne specifikace: auth v2 s klicem zarizeni
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
par klicu ECDSA P-256. Jmeno a heslo se pouziji jedinkrat, pri registraci klice,
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 hole r||s, 64 bajtu. Node podepisuje ve vychozim nastaveni do DER a ten
  by protistrana neuznala
- src/mcp/easyweb/device.ts - klic se vyrobi jednou a prezije restart, uklada se
  mezi udaje konektoru, ktere uz jsou zasifrovane. Novy priznak `managed` na
  poli sluzby znamena, ze ho vyplnuje portal a ve formulari se nezobrazuje
- 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 pravidla nejsou opatrnost navic: tokeny jsou jednorazove, druhe
pouziti server odmita kodem 409 a umi zarizeni zablokovat.

Transport:
- server si sam vybira, jestli odpovi JSON telem nebo SSE streamem, a streamem
  odpovida i na obycejna volani. Klient nabizi obojí a cte stream po kouscich -
  u dlouhych uloh ho server sam nezavira
- handshake plati na token, ne na volani
- odmitnute sezeni prijde jako chyba -32008 uvnitr uspesne odpovedi
- seznamy se skladaji pres vsechny stranky, bez toho je videt jen prvni
- odpoved se rozbaluje rekurzivne (structuredContent, contents, content, JSON
  zapsany jako text)

Dlouho bezici nastroje se spousti jako uloha a ceka se na ni dotazovanim. Limit
kroku se pri tom posouva z patnacti sekund na deset minut.

Nedodelane: trvaly kanal notifikaci (GET SSE), nahravani souboru po castech
a hlidani zmen kontraktu podle verze serveru.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 10:58:24 +02:00
JiriUhlirandClaude Opus 5 d881dab30d MCP EasyWeb: srovnani adresy a srozumitelna hlaska u HTTP 400
Overeno proti bezicimu serveru (samolepak klon z jejich settings.json):
/login vraci 400 Bad Request na kazde udaje - spravne heslo, spatne heslo,
prazdne heslo i neznamy ucet dostanou tutez odpoved, telo je vzdycky jen
"Bad Request" v HTML. Bez hlavicky Authorization vraci 401, takze handler
udaje cte a odmita je az uvnitr.

Tvar pozadavku tedy sedi: totez posila jejich vlastni WPF klient a stejny 400
vraci i holy curl. Zmeny jsou proto v tom, aby z toho clovek poznal, co se
deje:

- 400 i 401 z /login hlasi jako odmitnute prihlaseni a rovnou rikaji, ze
  tenhle server odpovida stejnym kodem i na neznamy ucet. Driv to bylo holé
  "vratilo HTTP 400", coz posilalo hledat chybu v adrese
- adresa se u EasyWebu srovnava jako v jejich klientovi: vlepena prihlasovaci
  cesta se odrizne a chybejici /mcp se doplni. Server je na cestu prisny,
  i /mcp/login/ s lomitkem vraci 404
- u obecne sluzby se adresa nemeni. Cizi server muze mit endpoint kdekoliv
  a "opravit" mu ji podle naseho odhadu znamena rozbit napojeni, ktere by
  jinak fungovalo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 10:21:48 +02:00
JiriUhlirandClaude Opus 5 81e4348ad8 Dve MCP sluzby: obecna podle specifikace a MCP 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 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 prave je.

Obecna sluzba 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 na jinem miste
- sluzba MCP EasyWeb: adresa, jmeno, heslo, nazev a otisk zarizeni.
  Prihlasovaci adresy si portal odvodi sam, otisk doplni z ID konektoru
- hotovy token u obecne sluzby. Rada verejnych serveru nic jineho nenabizi
- objevovani pres WWW-Authenticate, coz specifikace ma jako povinnou cestu.
  Pouziva se az kdyz obvykla mista selzou, stoji to volani navic
- zivotnost z tela tokenu: kdyz server expires_in ani datum neposle, cte se
  exp z JWT. 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.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 10:13:33 +02:00
JiriUhlirandClaude Opus 5 92fecca70c MCP: prihlaseni jmenem a heslem, tokeny si obstarava portal
Predchozi verze chtela po uzivateli token. 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. Obstarat ho, hlidat
platnost a vcas ho obnovit je prace portalu.

Novy src/mcp/auth.ts:
- 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 drzi
otisk udaju, takze zmena hesla ulozeny token zneplatni.

Udaje konektoru jsou ted adresa, jmeno, heslo a dve nepovinna pole (adresa pro
prihlaseni a rozsah opravneni). Ani jedno nemiri do hlavicky - Authorization se
pocita az pri volani z toho, co vydal autorizacni server.

Zpusob prihlaseni se pise do hlasky u konektoru: uzivatel vyplnil jmeno a heslo
a ma vedet, jak s nimi portal nalozil, nez zacne hledat chybu jinde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 09:52:23 +02:00
JiriUhlirandClaude Opus 5 435e254c90 MCP konektory: nacte nastroje ze serveru a udela z nich kroky
Firma si zalozi napojeni na svuj MCP server, stiskne Nacist nastroje a jeho
nastroje se objevi v builderu jako kroky automatizace vcetne toho, jake
promenne prijimaji a jake vraceji.

Pribylo:
- sluzba `mcp` - jedina v katalogu bez pevnych operaci, rekne je az server.
  Udaje: 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 u MCP neni tlacitko Overit
- src/mcp/client.ts - handshake, sezeni z hlavicky odpovedi, odpoved jako JSON
  i jako SSE stream, strankovani nastroju, nic z toho nevyhazuje vyjimku
- 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"}
- src/data/mcpTools.ts - nastroje v katalogu, kes nad tim, co je u konektoru
- sloupec `mcp` u konektoru (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

Rozhodnuti:
- nastroj patri firme, ne katalogu. serviceCatalog(tenantId) bez firmy nevrati
  zadny, takze zapomenuty argument znamena "nic", ne "vsechno"
- ID operace nese ID konektoru (tool:<konektor>:<nastroj>), protoze firma muze
  mit dva servery a na obou nastroj `search`
- krok se neopakuje, MCP nema idempotencni klic
- chyba nemaze nastroje, vypadek serveru nesmi vymazat kroky z automatizaci
- servery se pri startu neobvolavaji, jeden nedostupny by shodil katalog vsem

Dokumentace: prepsany 24-mcp-konektory.md na skutecny stav, novy
00-pro-programatory.md (rozcestnik, model ctyr pojmu, pravidla, ktera plati
vsude, co je krehke), doplnene 01, 12 a 99.

Mimochodem opraveno: setStatus v connectors/postgres.ts melo v RETURNING
doslovny retezec ${COLUMNS} misto dosazeni, a dva odstavce v dokumentu 12 byly
dvakrat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 09:43:03 +02:00
JiriUhlirandClaude Opus 5 3e6b365eec Prepsany navrh MCP: patri k modelu, ne do vyberu kroku
Prvni verze byla postavena spatne. Davala MCP nastroje do vyberu kroku, tedy
nastroj vybiral clovek a vyplnil mu pevna pole. Tak MCP nedava nic navic proti
HTTP konektoru, ktery uz mame - je to protokol pro modely, kde si nastroj
vybira model podle toho, co je zrovna potreba. Byla to zamena kategorie.

Spravne: MCP konektor neni zdroj kroku, je to schopnost, kterou dostane krok
s modelem. V builderu se objevi krok "Nechat model splnit ukol" a v nem se
zaskrtne, ktera napojeni smi pouzit. Krok vraci vysledek plus seznam toho, co
model opravdu zavolal - bez nej je to cerna skrinka a do provozu to nepatri.

Dokument popisuje dve cesty, lisi se tim kudy tece token zakaznika:
- predat server modelu (OpenAI ho zavola sam) - malo prace, ale token jde do
  OpenAI, server musi byt dostupny z jeho site a volani nejdou pres nas log
- byt MCP klientem my - vic prace, ale token zustava u nas, plati nase stropy,
  redakce i seznamy povolenych IP, a funguje to s jakymkoliv modelem

Doporucena je druha. Tvar tool objektu overen proti dokumentaci OpenAI
(type, server_label, server_url, headers, authorization, allowed_tools,
require_approval).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 09:15:11 +02:00
JiriUhlirandClaude Opus 5 2d38ac1350 Navrh MCP konektoru
Neni to naprogramovane, je to navrh: documentation/24-mcp-konektory.md.

Podstata problemu: MCP server je katalog operaci, ktery se zepta az za behu,
kdezto nas katalog je znamy pri prekladu. Retezec service.actions ->
findOperation -> scriptIdFor se dnes cely pta statickeho katalogu, takze by
slo pouzit jen nastroj, ktery uz nekdo predem zapsal do kodu - presny opak
toho, o co jde.

Navrh: jedna sluzba mcp a kazdy server jako konektor pod ni. Nastroje se
doplni do katalogu pres withRuntimeOptions, tedy tim samym zpusobem, jakym uz
se doplnuji resitele, skupiny a typy ticketu. Schema vstupu se prevede
z JSON Schema na OperationField, co se neprevede skonci jako json.

Jen Streamable HTTP, ne stdio: stdio by znamenalo pustit zakaznikuv program
uvnitr naseho containeru. Adresa projde stejnou kontrolou vnitrni site jako
HTTP a SMTP. Idempotence u MCP nefunguje, proto by se krok neopakoval sam.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 09:09:28 +02:00
JiriUhlirandClaude Opus 5 e1c52e3da3 Transformace maji svou kategorii, pribylo XML
Odstranena sluzba AI zpracovani textu. Delala totez co OpenAI, jen jinymi
slovy, a dva zpusoby jak udelat jednu vec znamenaji jen otazku, ktery pouzit.
Ukazkove stromy a stopy, ktere na ni odkazovaly, miri na openai.chat.

- nova kategorie Transformace dat. Driv byly mezi obecnymi sluzbami vedle
  webhooku a pauzy; ve chvili kdy jich je vic nez tri, hleda je clovek 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 toho 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, jinak by 2024 a 2025 byly totez.
- navrh cele skupiny transformaci v 13-transformace-dat.md: formaty, pole
  a hodnoty, seznamy, text a kontrola, vcetne toho co v seznamu zamerne neni.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 09:05:27 +02:00
JiriUhlirandClaude Opus 5 aa0a783095 AI zpracovani textu funguje: klic, adresa a tri skripty
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.

- sluzba stoji na api.openai.com/v1 stejne jako OpenAI, overeni GET /models
- model se bere z konektoru (ctx.config.model), ne z kazdeho kroku zvlast
- 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. Na to jde ve strome navazat podminkou,
na volny text ne.

Zjisteno u toho a zapsane do 21-realne-sluzby.md: proxy vraci na neznamou
aplikaci 200 s prazdnym telem, takze overeni konektoru u sluzby bez
verifyPath projde i tam, kde nic nebezi. Zatim neopraveno.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 07:32:18 +02:00
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