# 99 - Zaznam zmen Nejnovejsi nahore. ## 2026-09-09 - Prepinani jazyku docasne schovane Do rozhodnuti o nove znacce je web jen cesky. `MULTILANG_ENABLED = false` v `web/src/i18n/index.ts` schova prepinac v hlavicce i v mobilnim menu a `detect()` vraci vzdy `cs`; ulozena volba `en` se ignoruje, jinak by nekdo zustal v anglictine bez cesty zpet. Slovniky a `LanguageSwitch` zustavaji, zapnuti je jedna konstanta. Navrh znacky je zatim jen artefakt "Navrh znacky", do projektu se neaplikoval. ## 2026-09-09 - Resitel je clenstvi uctu, ne vlastni zaznam Prvni priznak: osoby zalozene pres ARES dostaly ucet, ale v Lidech nebyl nikdo, ani ten, za koho se spravce prepnul. Puvodni oprava chtela k uctu dozakladat zaznam resitele. Pri ni se ukazalo, ze chyba je hloubeji: resitel byl vlastni zaznam (`ppl_...`) spojeny s uctem **pres e-mail**, a e-mail je prihlasovaci jmeno, ktere spravce muze zmenit. Zmena adresy vazbu tise rozbila - clovek prestal videt "moje tickety" a nikdo nevedel proc. Kazde misto, ktere zaklada ucet (pozvanka, ARES, Nastaveni), navic muselo pamatovat na druhy zaznam a jedno vzdycky zapomnelo. Rozhodnuti: spojovat pres ID misto e-mailu by znamenalo dal drzet dva zaznamy o jednom cloveku. Oba se proto slily: **resitel je clenstvi uctu ve firme**, kazdy clen muze mit tickety u sebe, ID resitele je ID uctu. ### Model `Membership` (`src/shared/users.ts`) ma navic `role` (popisek), `capacity` (vychozi 8) a `externalIds`. `Person` (`src/shared/people.ts`) je jen pohled na jedno clenstvi: `id` = ID uctu, `tenantId` = firma clenstvi, jmeno a e-mail z uctu, `enabled` = stav uctu, `roleIds` = role clenstvi. Tentyz clovek ve dvou firmach je dvakrat se stejnym `id`. `src/data/people.ts` uz nic neuklada: zmizely `personStore`, `seedPeople`, `refreshPeople` a `findPersonByEmail`. Pohled sklada jedine misto, `personView`; k tomu `listPeople(tenantIds)`, `listAllPeople`, `findPerson(id, tenantId)` (neclen je `undefined`), `personName(id)`, `findPersonByExternalId(value, tenantIds)` a `personIdFor(user, tenantId)`. Skupiny (`personGroup`) zustavaji, clenove odkazuji na ID uctu. ### Migrace starych dat `src/data/migratePeople.ts` bezi pri startu z `bootstrapData`, az po nacteni ticketu a automatizaci, a jen kdyz ma stara kolekce `person` zaznamy. Ke kazdemu najde ucet podle e-mailu, nebo ho zalozi (nahodne heslo, `enabled` podle resitele, clenstvi `role_agent`), prenese popisek, kapacitu a externi ID na clenstvi a prepise `ppl_x -> usr_y` v ticketech (`assigneeId`, `resolvedById`), stromech automatizaci (nahrada v JSON), skupinach, telech akci a zdrojich vlastnich widgetu. Prevedene zaznamy smaze a zaloguje `[migrace] resitele -> ucty: ...`. Chyba jen loguje, zaznam se zkusi znovu pri dalsim startu. Historicke udalosti v logu ticketu si stara ID nechavaji. ### API a web `/api/dashboard/settings/people` ma stejne cesty a pravo `people.manage`, ale pod nim jsou ucty: `POST` zalozi ucet (heslo nahodne, kdyz chybi, role `role_agent`) s clenstvim, nebo prida clenstvi uctu s tim e-mailem; `PATCH` meni jmeno, e-mail (unikatni) a `enabled` na uctu a popisek, kapacitu, externi ID a role na clenstvi; `DELETE` odebere jen clenstvi a ucet bez clenstvi bez `platformAdmin` vypne. Udalosti `person.*` nesou `{ id, person }`. `/users` bere u clenstvi i `seesAllTenant`, `role`, `capacity`, `externalIds` a pri uprave je zachova. Pozvanka prisla o `asPerson` (prijme se a ignoruje). ARES zaklada ucet `role_admin` s popiskem clenstvi z funkci v rejstriku. V portalu spravce v Lidech upravuje cleny (pole vyse, role jako vyber vice hodnot), `InvitePanel` ztratil prepinac "zalozit i jako resitele" a texty o "resiteli bez uctu" a "spojce pres e-mail" jsou pryc. `hasPerson` pro katalog widgetu a vychozi rozlozeni znamena "je clenem firmy". ### Ukazkova data Ukazkovi resitele jsou ukazkove ucty (heslo `demo1234`): `usr_1` admin@automia.cz (Vedouci tymu, 5), `usr_2` karel.vomacka@automia.cz (Automia Servicedesk 8, Nordis Spravce servicedesku 8), `usr_3` martin.kriz@automia.cz (Integrace a API, 6), `usr_novakova`, `usr_bartos`, `usr_horakova`, `usr_kadlec`. Ukazkove tickety, automatizace a skupina `grp_servicedesk` odkazuji na ne. ### Ucet ve vic firmach Ucet muze byt ve vic firmach, proto se v Lidech odebira jen clenstvi (`DELETE /settings/people/:id`), ne ucet. Ze stejneho duvodu je i `enabled` na clenstvi: spravce firmy vypne cloveka u sebe, ne v jine firme, kde pracuje dal. Cely ucet vypina jen sprava uzivatelu. `Person.enabled` je `ucet.enabled && clenstvi.enabled !== false`; vypnute clenstvi se nenabizi k prirazeni ani nehleda podle externiho ID. ## 2026-09-09 - Novy ucet videl na dashboardu chybejici widget Kdo se prihlasil do nove firmy (nebo jako nove zalozeny ucet), videl jako prvni vec na prehledu hlasku "Widget list.myTickets uz v katalogu neni". Katalog widgetu tenhle widget schovava tomu, kdo ve firme neni veden jako resitel (nema co ukazat), ale **vychozi rozlozeni ho obsahovalo vzdy**. Dve pravidla o jedne veci na dvou mistech. `getLayout` a `resetLayout` v `dashboardLayouts.ts` dostavaji stejne `hasPerson` jako katalog a vychozi rozlozeni bez resitele `list.myTickets` nema; misto nej jsou nezarazene tickety pres celou sirku. Ulozene rozlozeni se nemeni - kdyz nekomu resitele zrusi, hlaska s tlacitkem "Odebrat" je spravna, protoze si ten widget na dashboard dal sam. ## 2026-09-09 - Revize projektu: prava, vykon, runtime, portal a ARES Velka sada oprav napric celym projektem. Zadna nova obrazovka, ale skoro kazda vrstva se zmenila v tom, **co dela pri zatezi a pri chybe**. K tomu jedna nova funkce: zalozeni firmy podle registru ARES. Zaznam je dlouhy schvalne - tohle je misto, kde se za pul roku hleda, proc se neco chova tak, jak se chova. ### Uloziste: jeden rezim pro vsechno Rozhodnuti o rezimu (`postgres`, `file`, `memory`) delal `connectorStore.ts` pro konektory a `initStores` pro zbytek, kazdy podle svych podminek. Mohlo se stat, ze konektory jely z databaze a tickety ze souboru. Ted rozhoduje **jen `initStores` v `src/data/store/index.ts`**: Postgres jen kdyz je `DATABASE_URL`, migrace prosly a je cim sifrovat (`SECRETS_KEY`), jinak soubor nebo pamet pro vsechna uloziste vcetne konektoru. `connectorStore` uz jen vola `initStores`. Dalsi opravy v ulozisti, kazda ma za sebou konkretni problem: | Co | Proc | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | audit se maze davkou (`removeMany`) | orezavani mazalo jen radky platformy, audit firem rostl donekonecna. Bezi po 50 zapisech nebo nejvys jednou za minutu | | `markRead` pres `updateMany` | notifikace delaly jeden zapis a jednu obnovu cache na kazdy zaznam | | `persist` / `flushPersist` u ticketu | vic zmen tehoz ticketu v jednom tiku je jeden zapis, ne pet | | `withMirror` radi zapisy za sebou | dva `put` tehoz zaznamu mohl Postgres potvrdit v opacnem poradi a v tabulce zustala starsi verze. Ted je na kazde ID retez promise | | `issues` a `stepCount` ulozene | `listAutomations` validoval vsechny stromy pri kazdem cteni. Ted se spocitaji pri ulozeni (`withDerived`) a jednou pri startu | | registr cache (`refreshCache(kind)`) | route nastaveni obnovovala vsechny cache, ted jen tu entitu, do ktere psala (`bootstrapDataRefresh(route)`) | | `listByTenant(tenantIds, sortBy)` | sedm kopii filtr + razeni v modulech entit | | mapy misto `find` | `withCache.byId` je `Map`, ticketStore ma `ticketsById` a `ticketsByExternal`, vytizeni a statistiky se seskupi jednim pruchodem | | `create` v lokalnim ulozisti hazi na duplicitu | Postgres to delal, soubor tise prepsal | | `snapshot.ts` prepise soubor jen pri ENOENT | jina chyba cteni (prava, plny disk) driv znamenala start s prazdnymi daty a **prepsani souboru prazdnym obsahem**. Ted se zapisy zamknou a zaloguje se to | | `listIncidents(tenantIds)` povinne | stejne pravidlo jako u ticketu, incident byl posledni seznam bez filtru | Slovnik stavu ticketu je sjednoceny na cesky `defaultStatuses` (Novy, V reseni, Ceka na klienta, Vyreseno) a ukazkove widgety filtruji `closed: false`, ne podle nazvu stavu. Ukazkove tickety TK-4817 a TK-4812 vznikaji jen se `SEED_DEMO=1`. V `services.ts` byla dvakrat operace `set-status`, druha tise prekryvala prvni; `checkOperationIds()` to ted pri nacteni zaloguje. Spolecne pomocne funkce, aby se nepsaly po modulech: `nowIso`, `minutesAgo`, `highestNumber`, `writableOrWarn` v `store/types.ts`, `mergeValues` v `connectors/types.ts`. ### Runtime: worker je pool a opakuje se jen to, co muze pominout Worker bral davku ctyr behu a cekal, az dobehnou vsechny. Jeden pomaly beh tak blokoval tri volne sloty. A beh delsi nez deset minut se povazoval za zaseknuty, vratil se do fronty a **vykonal se podruhe**. Ted: - `active` je mnozina bezicich ID, kazde kolo si vezme `CONCURRENCY - active.size` behu a spusti je bez cekani na ostatni, - `claimBatch(limit, active)` preskakuje to, co uz bezi, - beh kazdou minutu posle tlukot (`touchClaim`) a za zaseknuty se povazuje az 30 minut od posledniho tlukotu (`STUCK_AFTER_MS`), ne od vzeti z fronty. **Opakovani.** Kazda chyba skriptu se opakovala petkrat za 72 minut, i 403 a spatny vstup. Ted se krok opakuje jen kdyz sam rekne `retryable`: chyba spojeni, timeout, 5xx a 429, vypadek uloziste konektoru. 401, 403, 404, validace a `ctx.fail` konci hned a zakladaji incident. U MCP jsou opakovatelne chyby spojeni a 408, 425, 429, 502, 503, 504; **timeout uz odeslaneho `tools/call` opakovatelny neni**, protoze MCP nema idempotencni klic a nastroj by se provedl podruhe. Pravidlo je v komentari nad `StepResult` v `executor.ts`, aby ho nasel kazdy, kdo pise novy druh kroku. Dalsi zmeny v behu: | Co | Proc | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `MAX_ACTIONS = 1000` | `MAX_STEPS = 50` pocita staticky strom. Smycka se dvema kroky nad 26 polozkami narazila na 50 a beh spadl. Vykonane kroky maji vlastni strop | | datum v podmince pres `Date.parse` | `gt` a `lt` nad datem prevadely ISO retezec cislem, vyslo NaN a podminka byla vzdycky nesplnena | | `publishOutputs()` spolecne | vestavene kroky a skripty publikovaly vystupy kazdy jinak. Hole jmeno se zapise jen kdyz v kontextu jeste neni - nastroj MCP vracejici `status` prepisoval `status` spoustece | | sandbox stavi `utils` i `input` uvnitr vm | funkce hostitele prosakovaly do skriptu firmy a `constructor('return process')` z nich utekl ven. Zkompilovane skripty se cachuji (LRU 100) | | chybejici nebo pozastavena automatizace | neopakovatelna chyba plus incident, driv se to zkouselo dokola | | `incident/create` nese `tenantId` | krok zakladal globalni incident, ktery videly vsechny firmy | | planovac zarazuje s triggerem `poll` | bylo `manual`, takze se v behu nedalo poznat, ze to spustil planovac | ### Sit a tajemstvi Novy `src/net/guard.ts` sdruzuje to, co melo kazde volani ven zvlast: `assertAllowedUrl` (zakaz vnitrni site), `describeFetchError`, `readBodyLimited` a `readJsonLimited`. Telo se **cte proudem a usekne se u limitu** - driv se nacetlo cele a teprve pak zmerilo, takze limit nechranil pamet. Pouziva to HTTP skriptu, klient MCP, prihlaseni MCP i SMTP. Redaktor masky navic maskuje tajemstvi v **URL-encoded a JSON-escaped** tvaru, protoze cizi sluzby je v chybach vraceji i tak. `ctx.config` skriptu uz nikdy neobsahuje tajna pole (`scriptConfig`). `EasyWebAuthError` dedi z `AuthFailure` (novy `src/mcp/errors.ts`), takze se chyby prihlaseni poznaji jednou kontrolou a detail je vzdy zredigovany. Klient MCP: handshake se cachuje i pro server bez prihlaseni, `Mcp-Session-Id` se uklada s handshakem a posila znovu, 400 nebo 404 po preskocenem handshaku vyvola jeden novy handshake. OAuth prihlaseni sdili rozdelanou operaci na konektor. V `delay()` unikal posluchac abortu. `src/scripts/util.ts` dostal `pick`, `pickText`, `jwtExpiry`, `parseBool`, `parseNumber` a jednu konstantu `DETAIL_BYTES` na zkracovani (MCP mel 600 znaku, zbytek 8 kB). `ctx.util` skriptu ma navic `day`, `list`, `addresses`, `quote`; sablona `scripts/_sablona.js` je vypisuje a osm skriptu je pouziva. ### API: prava se kontroluji za firmu a u kazde route Prava byla ve vetsine rout jen "je prihlaseny" nebo "je clen firmy". Ted: | Route | Kdo smi | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | firmy CRUD | jen spravce platformy | | uzivatele CRUD | spravce platformy vse. Spravce firmy (`user.manage`) jen lidi sve firmy, nenastavi `platformAdmin`, neprida clenstvi jinde, nesahne na spravce platformy a nesmaze cloveka, ktery je i v jine firme | | pozvanky | `user.manage` a jen role te firmy | | konektory create, update, delete, test | `connector.manage` | | automatizace create, update, delete, regenerate | `automation.edit` za firmu automatizace | | `/services`, `/connectors/services` | clenstvi ve firme, cizi firma je 404 | | assign, status, comment, claim | `builtinAction` v `ticketActions.ts`, pravo za firmu ticketu a strop viditelnosti (`visibleTicketOrDeny`) | | `/api/admin/*` | `impersonate` a `audit.view` se ted opravdu kontroluji | Nove middleware, kazde s jednim ukolem: | Soubor | Co | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `middleware/asyncHandler.ts` | `wrap`, `safeRouter`: odmitnuta promise v handleru driv zabila proces. `unhandledRejection` se loguje, `uncaughtException` loguje a ukonci | | `middleware/rateLimit.ts` | klouzave okno v pameti: login 20 za 15 min, kontakt 5 za hodinu, prijeti pozvanky 5 za 15 min. 429 s `Retry-After` | | `middleware/tenant.ts` | `attachAccess` spocita pristup jednou na request do `req.access`; `tenantOrDeny`, `optionalTenantOrDeny`, `scopeOrDeny` misto kopii v routach | | `middleware/validation.ts` | `validationError`, jeden tvar `{ error, message, issues: [{ field, message }] }` | | `lib/secure.ts` | `timingSafeEqualString` pro tokeny webhooku, prijmu a pozvanek | V `index.ts`: log requestu maskuje tokeny za `/webhook/`, `/webhook/ticket/` a `/invites/`; bezpecnostni hlavicky (nosniff, `X-Frame-Options SAMEORIGIN`, `Referrer-Policy`, `Permissions-Policy`); `trust proxy` = 1, aby limit pocital s adresou klienta a ne proxy; rozpoznani API 404 bere `config.rootPath` misto natvrdo `/apps/`. Dockerfile instaluje `npm ci`. Vykon: data widgetu nactou seznam ticketu jednou na request, ne za kazdy widget; `/tickets` a `/runs` berou `limit` a `offset` (nejvys 500) a vraceji `X-Total-Count`, `/tickets` i `total` v tele; `hashPassword` je asynchronni, aby bcrypt neblokoval smycku; `/people/:id` prochazi tickety jednou. `GET /api/dashboard/access` vraci `roleNames`, aby klient nehadal popisek role. `/storage` a `/scripts` vraceji cesty na serveru jen spravci platformy. V `openapi.ts` pribylo 22 chybejicich cest, `/whoami` je opraveny a `features` uz nejsou popsane jako CRUD. ### Udalosti nesou firmu `DashboardEvent.tenantId` (null = cela platforma). Stream SSE filtruje zive udalosti i historii podle firem uzivatele, udalost s `payload.userId` jde jen tomu cloveku. Driv videl kazdy prihlaseny udalosti vsech firem. Nove udalosti entit `tenant|user|role|person|group|ticketType|action|widget|connector|feature` s `.created|.updated|.deleted`, publikuje je `crudRouter` (volba `event`), routy konektoru a PUT features. Payload je `{ id, : zaznam }`, u smazani `{ id }`. Udalosti ticketu `ticket.updated`, `ticket.assigned`, `ticket.resolved` nesou v `payload.ticket` cely ticket, takze klient opravi seznam na miste a nemusi se ptat znovu. ### Portal: obnova bez odmontovani a klientsky sklad ciselniku Kazda udalost ze streamu odmontovala stranku: `useApiQuery` prepnul `loading` a `DataState` vykreslil spinner misto deti. Ted je `loading` jen do prvnich dat, potom `refreshing`, a deti zustavaji. Hooky sdili jeden debounce 150 ms, cache modulu a deduplikaci bezicich dotazu (klic firma + cesta + telo). `patchOn` opravi data v cache z udalosti (`lib/ticketEvents.ts` bere `payload.ticket`). Rozhrani: ```ts useApiQuery(path, { refetchOn?, body?, enabled?, patchOn? }) -> { data, loading, refreshing, error, total, reload } ``` **Rozhodnuti majitele produktu: stredni cesta.** Ciselniky (lide, skupiny, typy ticketu, sluzby, konektory, pristup) jsou v klientskem skladu `lib/collections.tsx`: nacitaji se line pri prvnim pouziti, mazou se pri prepnuti firmy a odhlaseni, opravuji se z udalosti entit (upsert nebo smazani ze zaznamu v payloadu, jinak jedno nacteni te kolekce). Tickety, behy a statistiky **zustavaji dotazy na server** se strankovanim - jsou velke a meni se porad. Hooky: `useCollection(key)`, `useAccess()`, `useCollectionSelector`. Dalsi opravy klienta: | Co | Proc | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | 401 maze token a vyvola `auth:expired` | po vyprseni tokenu portal ukazoval prazdne stranky. Login rekne "Prihlaseni vyprselo", stream se prestane pripojovat na 401 a 403 | | `restore()` maze token jen na 401 | vypadek site pri startu odhlasoval | | `onClose` modalu v ref | fokus se pri kazdem prekresleni vracel na zacatek | | toast ma jeden casovac | dva toasty za sebou si rusily odpocet | | zrusene asynchronni efekty | odpoved pro uz odmontovanou stranku prepisovala stav te nove | | tiche `catch` nahrazene chybou | pravidlo "zadna ticha selhani" platilo na serveru, na klientovi ne vsude | | filtry ticketu v URL | nalez slo poslat kolegovi a vratit se pres zpet | | detail ticketu neblokuje chyba `/people` | jeden padly dotaz na ciselnik schoval cely ticket | | `MappingEditor` stabilni klice radku | smazani radku prekreslilo vsechny nasledujici a ztratil se kurzor | | `lib/useUnsavedChanges.ts` | odchod z rozepsaneho builderu bez varovani | | `DashboardLayout` lazy, sourcemapy vypnute | verejny web nenacital kod portalu, produkce neposila zdrojaky | | `TicketTable` tabulka nebo karty | `useMediaQuery` misto duplicitni komponenty | **Builder.** `collectScopes` memoizovane, karty v `memo`, callbacky podle ID kroku; `FlowCanvas` je rozdeleny do `flow/{ActionCard,ConditionCard,ForeachCard,StepControls}.tsx` a `canvasTypes.ts`. Stranka Konektory je rozdelena do `pages/dashboard/connectors/{ConnectorCard,ConnectorEditor,ConnectorLogs,ConnectorTools}.tsx`. Pred tim byl kazdy stisk klavesy ve strome o padesati krocich prekresleni vseho. **Formularova vrstva** podle navrhu v dokumentu 25, sekce 2, je hotova: `components/ui/form/{controlClass,Field,Input,Select,Textarea}.tsx`, `lib/useSubmit.ts`, `lib/options.ts`, `components/ui/Chip.tsx`, `components/dashboard/TicketCard.tsx` (kompaktni varianta), `plural()` v `lib/format.ts`. Zmizelo 15 kopii trid vstupniho pole. **Jazyky.** Verejne stranky (Postup, Produkty, Reference, O nas, Kontakt, 404, Prihlaseni, Sluzby, paticka, navigace) plus `DataState` a `ErrorBoundary` jdou pres i18n a `en.ts` je pro ne uplna. **Sdilene typy**: ciste typove moduly v `src/shared/*.ts` (16 souboru, vcetne `users.ts` pro ucet a clenstvi) jsou jediny zdroj typu API. `web/src/types/dashboard.ts`, `events.ts` i `AuthContext` je re-exportuji pres alias `@shared/*` (`web/tsconfig.json` paths a `vite.config.ts` alias). Pri prevodu se nasly rozjete tvary, vsechny vyresene ve prospech serveru: webovy `Ticket` nemel `createdById`, `Access.roleNames` bylo nepovinne, `Person` nemel `enabled`, `Incident` nemel `tenantId` ani `source`, `Service` neznal kategorii `transformace`, seznam operatoru podminky u typu `list` na webu nemel `contains`, takze builder nenabizel podminku nad stitky, ktera na serveru funguje. Serverovy ulozeny tvar (`StoredTicket`, `Connector` s `values`) zustava na serveru; web dostava `PublicConnector` jako `Connector`. Pravidlo od ted: novy typ odpovedi patri do `src/shared`, web ho nekopiruje. ### Nova funkce: firma z registru ARES Zalozit firmu znamenalo opsat nazev, IC, DIC a adresu rucne a pak zvlast zakladat ucty. Ted je to na `/api/dashboard/settings/ares`, jen pro spravce platformy: | Endpoint | Co | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `GET ares/companies?query=` | podle IC presne, jinak podle nazvu. U firmy, ktera uz v portalu je, vraci `existingTenantId` | | `GET ares/companies/{ico}/persons` | soucasni clenove statutarniho organu a prokura z verejneho rejstriku, kazdy s navrzenym e-mailem `IC-poradi@placeholder.cz` | | `POST ares/tenants` | zalozi firmu s `ico`, `dic`, `address`, `legalForm` z ARES a ucty vybranych osob, vsechny s roli `role_admin` v nove firme, nahodne heslo, audit `tenant.create.ares` | Zaznam firmy ma nove nepovinne `ico`, `dic`, `address`, `legalForm`; CRUD firem hlida unikatni IC. Adresa registru je `ARES_BASE_URL`, vychozi `https://ares.gov.cz/ekonomicke-subjekty-v-be/rest`. Klient ARES pouziva tentyz `net/guard.ts` jako vsechno ostatni, co vola ven. **Rozhodnuti o rolich:** spravce platformy zaklada firmy, spravce firmy pak spravuje skupiny, vedouci a cleny uvnitr firmy. Rejstrik nezna e-maily, proto zastupne adresy - **spravce je musi nahradit skutecnymi**, jinak se ti lide neprihlasi a nedostanou pozvanku. ## 2026-09-08 - Otevrena stranka sekala prehravani videa Pri otevrenem portalu zacalo vedle nej sekat prehravani videa, po zavreni stranky bylo hned dobre. Neslo o smycku v kodu: **zivy prenos udalosti je SSE, jedno spojeni na celou aplikaci**, a `setTimeout` je v nem jen na odstup pri znovupripojeni. Zdrzeni bylo jinde a byly to tri veci. ### Rozmazavani plne barvy `.glass`, tedy podklad skoro kazdeho panelu, mel `backdrop-filter: blur(14px)`. Jenze **pod kartami je plna barva** (`bg-ink-950` na dashboardu), takze se rozmazavala jednolita plocha: zadny rozdil videt nebyl a kazda karta si za to drzela vlastni rozmazanou vrstvu ve skladaci. V kodu je `glass` na 58 mistech, na jedne obrazovce jich je snadno dvacet. Rozmazani zustava tam, kde pod nim neco opravdu projizdi: lepici hlavicka dashboardu i webu, podklad modalu a prekryv menu na mobilu. To je pet mist, kazde se svym `backdrop-blur-*` primo u sebe. ### Nekonecne animace nad velkymi rozmazanymi plochami Prihlaseni a verejny web meli zare o velikosti 26 az 34 rem s `blur(120px)`, kterym se **donekonecna animovala pruhlednost**. Animovat pruhlednost silne rozmazaneho objektu znamena prepocitavat obrovskou vrstvu sedesatkrat za sekundu, porad, i kdyz se na strance nic nedeje. Zare zustaly, animace zmizela. Krome toho pribylo `prefers-reduced-motion`: kdo si v systemu animace vypnul, nema je dostat ani tady. ### Kazda udalost prekreslila cely dashboard `EventStreamProvider` mel v tomtez kontextu **stav spojeni, odber i seznam poslednich udalosti**. Seznam se meni pri kazde udalosti, takze se menila cela hodnota kontextu - a prekreslil se kazdy, kdo si jen zaridil odber. To jsou vsechny dotazy s `refetchOn` na strance, i kdyz o ten typ udalosti nestaly. Pri behu automatizace, ktera posila `automation.run` a `ticket.updated`, se tak cely dashboard prekresloval nekolikrat za sekundu - a s nim dvacet rozmazanych vrstev. Kontext je proto rozdeleny: `useEventStream` da stav a odber, `useEventLog` seznam udalosti. Seznam si bere jen ten, kdo ho opravdu vypisuje, tedy toasty. ### Filtr sekci nefiltroval Seznam ticketu sklada adresu dotazu v `useMemo`, ale v zavislostech chybely `groupId`, `typeId`, `tag` a `stage`. Adresa se tedy po prepnuti sekce neprepocitala a data se nenacetla znovu - filtr vypadal, ze nedela nic. Driv to nevadilo: byly to filtry, ktere sly nastavit jen odkazem a po nacteni stranky se uz nemenily. Se zalozkou "Vsechny tickety" a jejim vyberem sekce se z toho stala chyba, ktera je videt. Overeno: `groupId=grp_servicedesk` vrati jeden ticket, `groupId=none` zbyle dva. ## 2026-09-07 - Podminka se muze ptat na vic veci naraz Podminka byla **prave jedna otazka**. Slozitejsi vetveni se muselo skladat z vnorenych podminek, takze "vysledek dorazil" a v nem "vysledek je X" byly dve urovne stromu misto jedne vety. U tri hodnot, ktere maji dopadnout stejne, to byly tri urovne, ve kterych se nikdo nevyzna. ### Model ```ts rules: ConditionRule[] // otazky match: 'all' | 'any' // a zaroven / nebo ``` Stara podoba (`fieldId`, `operator`, `value` primo na kroku) se dal cte, prevadi ji `rulesOf` - **jedine misto, kde se to deje**. Kdyby se `fieldId` cetlo primo, krok ulozeny driv by po zmene modelu prisel o svou otazku a vetvil by vzdycky stejne, tise a bez chyby. Zapisuje se uz vzdycky `rules`. Aby se na zadne cteni nezapomnelo, je `fieldId` v typu **nepovinne**. Prekladac tim ukazal vsech pet mist, ktera podminku ctou: vyhodnoceni, obe hlasky do logu, kontrola pri ukladani a kontrola nedodelku. ### Proc jedna uroven a ne vyrazy se zavorkami Dve treti podminek jsou "vsechny tohle" nebo "cokoliv z tohohle". Zavorky by v rozhrani znamenaly editor vyrazu, ktery uz nikdo neuklika, a textovy zapis by navic zahodil to podstatne: **odkaz na parametr pres ID**. Diky nemu prejmenovani parametru podminku nerozbije a builder umi nabidnout jen operatory, ktere na dany typ sedi, a rovnou rict, ze se odkazuje na parametr, ktery vznika az pozdeji. Az se ukaze, ze jedna uroven nestaci, da se textovy zapis pridat nad tentyz vyhodnocovac. Opacne to nejde. ### V builderu Radek na otazku, k tomu tlacitko "Přidat otázku" a od druhe otazky prepinac **sedí všechny** nebo **sedí aspoň jedna**. U jedne otazky se prepinac nenabizi, nema co spojovat. Posledni otazka nejde smazat: podminka bez otazky by tise nevetvila, proto ji runtime rovnou povazuje za nesplnenou a zaloguje to. ### V logu Radek podminky nese vsechny otazky i s tim, ktera rozhodla: ```text Podmínka: result isNotEmpty a zároveň result eq Chybějící informace: nesplněno result = "Přesměrování": sedí result = "Přesměrování", porovnáno s "Chybějící informace": nesedí ``` Bez rozpadu by u spojene podminky bylo videt jen "nesplneno" a ne to, ktera otazka to zpusobila. ### Overeno na bezici instanci Strom se dvema spojenymi podminkami, pet prichozich volani: | Co prislo | Prirazeno | Vyrizeny | | --------------------------- | ------------- | -------- | | vysledek nedorazil | ne | ne | | Chybějící informace | ano | ne | | Přesměrování | ne | ano | | Mimo téma | ne | ano | | Něco jiného | ne | ne | Prvni podminka je `all` (dorazil a zaroven je to Chybejici informace), druha `any` (Přesměrování nebo Vyřešeno nebo Mimo téma). Strom ulozeny ve stare podobe se nacte a ulozi beze zmeny. ## 2026-09-07 - Automatizace zavirala tickety uz pri zvoneni Na instanci nebyl ani jeden nevyrizeny ticket: **vsech 131 melo `closed: true`**, takze dlazdice "Moje tickety" i "Fronta bez resitele" ukazovaly nulu. Widget pocital spravne, spatna byla data. ### Cim to bylo Strom se ptal `result neq "Chybějící informace"` a ve vetvi ANO ticket zaviral. Jenze jeden hovor posle vic zprav a **ta prvni jen ohlasi, ze zacal**: `data` je null, takze `{{result}}` je prazdne. `evaluate` prevadi chybejici hodnotu na prazdny retezec, a prazdno se opravdu nerovna "Chybějící informace" - podminka tedy sedla a ticket se zavrel uz pri zvoneni. Vsechno ostatni, co pak prislo, uz jen doplnovalo zavreny ticket. Odtud i druha vec: **prirazovaly se i tickety, ktere nemely.** Jedna zprava hovoru mela `result: "Chybějící informace"`, takze ticket sel na servicedesk a na cloveka. Dalsi zprava tehoz hovoru prinesla `"Přesměrování"` a ticket zavrela, ale **resitele uz neodebrala**. Z dvaceti ticketu prirazenych jednomu cloveku jich melo "Chybějící informace" jen deset. ### Oprava stromu Nejdriv se ptame, jestli vysledek vubec prisel, a teprve pak **kladne**, jestli je to "Chybějící informace": ```text result isNotEmpty ANO result eq "Chybějící informace" ANO priorita vysoka, sekce Servicedesk, prirazeni nejvolnejsimu NE zavrit NE nic, vysledek jeste nedorazil ``` Puvodni `neq` znamenalo "vsechno ostatni **vcetne toho, co jeste nevime**". To je u nepovinneho parametru, ktery dorazi az pozdejsi zpravou, past. ### Aby to slo poznat z logu Radek podminky rikal jen "splneno". Ted nese i to, **s cim se porovnavalo**, a rozlisuje nedorazilo od prazdneho: ```text Podmínka: result isNotEmpty: nesplněno | result = nedorazilo Podmínka: result eq Chybějící informace | result = "Přesměrování", porovnáno s "Chybějící informace" ``` Prvni radek je presne ta informace, ktera chybela. Bez ni log tvrdil, ze podminka sedi, a nikdo nemel jak zjistit proc. ### Overeno prehranim celeho hovoru | Zprava | Vysledek | | ----------------------------- | ----------------------------------------------- | | in-progress, bez dat | ticket vznikl, **neni vyrizeny**, bez resitele | | completed, Chybějící informace| priorita vysoka, prirazen clenu Servicedesku | | completed, Přesměrování | zavren | Pred opravou byl ticket vyrizeny uz po prvni zprave. ### Co zustava k rozhodnuti Kdyz se hovor prehodnoti z "Chybějící informace" na neco jineho, ticket se zavre, ale **resitel na nem zustane**. Do "Mych ticketu" uz nespadne, ty ukazuji jen nevyrizene, ale ve vsech ticketech u nej to jmeno stoji. Odebrat resitele pri zavreni by slo, jen na to zatim neni krok - `ticket/assign` bez resitele skonci chybou. ## 2026-09-02 - Nazev znacky uz neni v kodu natvrdo Server mel "Automia" napsanou primo v titulku Swaggeru a v OpenAPI - zbytek po prejmenovani, ktere probehlo jen na klientovi. Prejmenovat produkt tedy znamenalo hledat retezec po souborech. ### Dve promenne, a je to zamer | Kde | Promenna | Kdy se dosadi | Vychozi | | ------ | ----------------- | ------------- | ---------- | | server | `BRAND_NAME` | za behu | `WorkNuke` | | klient | `VITE_BRAND_NAME` | pri buildu | `WorkNuke` | Klient je **staticky soubor**, takze runtime promenne containeru do prohlizece nedosahnou - proto se u nej dosazuje pri buildu. Obe maji tutéž vychozi hodnotu, takze bez nastaveni cehokoliv sedi. Zbytek firemnich udaju (claim, kontakty, adresa) zustava v `web/src/config/brand.ts`. Do promennych to nepatri, meni se to jednou za rok a env by z toho udelalo deset promennych, ktere nikdo nenastavi. ### Co se timhle nemeni **Firma Automia v ukazkovych datech.** Pozvanka ukazuje jmeno firmy, do ktere zve, a ta se v seedu jmenuje Automia. Neni to zbytek po prejmenovani, je to zaznam zakaznika - zadna promenna znacky ho nezmeni a nema. Overeno: s `BRAND_NAME=ZkouskaZnacky` ma OpenAPI titulek "ZkouskaZnacky - portal a API" a Swagger "ZkouskaZnacky API". ## 2026-09-02 - "Interni chyba serveru" pri zalozeni ticketu Formular noveho ticketu koncil na 500 uz pri vyplnenem samotnem predmetu. ### Cim to bylo `apiFetch` prevadi telo na JSON sam: ```ts body: body !== undefined ? JSON.stringify(body) : undefined, ``` Dialog mu ho ale predaval **uz prevedene**, tedy `body: JSON.stringify({...})`. Druhy prevod z objektu udelal retezec a na server dorazilo `"{\"subject\":...}"`. `express.json` je ve vychozim nastaveni `strict`, takze retezec na nejvyssi urovni odmitne a vyhodi vyjimku. Tatáz chyba byla i ve dvou mistech helpdesku: zalozeni pozadavku a komentar. ### A druha polovina: spatna hlaska Vyjimka z parseru tela propadla do centralniho error handleru, ktery z nej udelal **500 "Interni chyba serveru"**. Ta hlaska rika, ze je neco spatne u nas, a posila cloveka hledat na spatnou stranu - pritom slo o spatne polozeny dotaz. Error handler proto rozpozna chyby parseru tela (`status 400` a `type` zacinajici `entity.`) a vraci **400 s vetou "Telo pozadavku neni platny JSON objekt."** Ostatni chyby zustavaji 500, jak byly. Overeno: telo zakodovane dvakrat vraci 400 s tou vetou, opravene telo jen s predmetem vraci ticket. ## 2026-09-02 - Builder nabizi jen to, co jde zavolat V nabidce kroku byly vsechny sluzby katalogu, i ty, ke kterym firma nema napojeni. Slo tedy vybrat Raynet CRM bez konektoru a postavit strom, ktery pri prvnim behu spadne na chybejicich udajich - a to se pozna az za tyden, kdyz prijde prvni ostra udalost. ### Pravidlo Nabizi se sluzba, ktera je **obecna, nebo k ni firma ma napojeni**. Obecne jsou ty, co se bez konektoru obejdou: webhook, casovac, rucni spusteni, formular, incident, ticket, HTTP, transformace, pauza a zapis do logu. Ma to uz priznak `general`, jen ho nikdo nepouzil na filtrovani nabidky. ### Katalog se nefiltruje, jen se oznacuje `GET /api/dashboard/services` prida ke kazde sluzbe `connected`, tedy jestli k ni firma ma aspon jedno napojeni. Sluzba z odpovedi **nemizi**: log ticketu a detail akce podle katalogu prekladaji ID operaci na jmena, a kdyby zmizela, zustalo by v uz zapsanem radku hole ID. Filtruje az builder. Aby nabidka nevypadala jako cely katalog, je pod ni veta, kolik sluzeb ceka na napojeni. Bez ni to vypada, ze sluzba neexistuje, misto ze k ni chybi udaje. ### Soukroma sluzba uz neni videt cizi firme `canSeeService` vracelo spravci platformy `true` driv, nez se vubec podivalo na viditelnost sluzby. Zakazkova integrace omezena na jednoho klienta se tak ukazovala i po prepnuti do jine firmy - spravce si ji mohl vybrat do jeji automatizace. **Rozhoduje firma, ne clovek.** Kdyz je firma vybrana, plati jeji seznam; bez vybrane firmy spravce platformy spravuje katalog a vidi vsechno. Zaroven se konecne pouziva `tenantHasService`, tedy zpristupneni sluzby firme pres nastaveni - dosud to bylo pole, ktere nikdo necetl. Overeno: Polstryn SAP je pro `tnt_logitrans` v katalogu, pro `tnt_automia` uz ne, a to i pro spravce platformy. V nabidce builderu pro `tnt_automia` zbylo deset obecnych sluzeb plus iDoklad, na ktery firma napojeni ma. ## 2026-09-02 - Tickety maji zalozky, fronta umi prirazovat Seznam ticketu mel dva prepinace, "Moje tickety" a "Ve fronte", schovane mezi ostatnimi filtry - splyvaly s nimi a vypadaly jako dalsi dva chipy. Jenze "co mam u sebe", "co jeste nikdo nema" a "co je ve firme" nejsou filtry, jsou to **tri ruzne prace a kazda chce jinou tabulku**. ### Tri zalozky | Zalozka | Kdo ji ma | Cim se lisi | | ---------------- | ----------------------------- | ------------------------------------ | | Moje tickety | kdo je veden jako resitel | bez sloupce Resi, byl by tam porad on | | Nezarazene | kazdy | radky s rychlym prirazenim | | Vsechny tickety | kdo vidi i cizi tickety | sloupec Resi a filtr na sekce | **Sloupec Resi je jen ve Vsech.** V Mych by ve vsech radcich stalo tyz jmeno a ve fronte je z definice prazdny. Presne o tom mluvil pozadavek: az v prehledu vsech ma smysl videt, kdo co ma u sebe. Zalozka Vsechny se nenabizi tomu, kdo vidi jen svoje - byl by to tentyz seznam. Rika to `Access.seesOthers`, a `Access.visibleGroups` k tomu prida sekce, ktere smi filtrovat: kdo vidi celou firmu vsechny, vedouci jen ty, ktere vede. Nabidnout mu sekci, ze ktere stejne nic neuvidi, je jen matouci. ### Fronta umi prirazovat Nova `QuickAssign`: u kazdeho ticketu ve fronte tlacitko **Vzit si** a dve roletky, komu a do ktere sekce. Prirazeni je ve fronte hlavni prace, ne jedna z mnoha veci na detailu - otevirat kvuli tomu kazdy ticket znamena u dvaceti ticketech ctyricet kliknuti navic. Dve roletky, ne jedna: clovek a sekce jsou dve ruzna rozhodnuti. "Tohle je pro ucetni" jde rict bez toho, aby se resilo, kdo z nich ma dovolenou. Kdo nesmi rozdavat praci ostatnim, vidi jen "Vzit si". Endpointy uz existovaly (`/claim`, `/assign`, `/group`), nova je jen cesta k nim. ### Vychozi zalozka podle adresy Odkaz z widgetu nese `assignee`, takze `unassigned` otevre frontu a konkretni resitel nebo sekce pohled Vsechny. Bez toho by clovek prisel z dlazdice "fronta bez resitele" a koukal na svoje tickety. Kdo neni veden jako resitel, nema zalozku Moje. Vychozi stav ji ale predpoklada, protoze prava dorazi az po prvnim vykresleni - proto pojistka, ktera po nacteni prav prepne na prvni dostupnou zalozku. ### Hledani s tim pocita Zapsano do navrhu: hledani ma zapadnout do zalozky, ve ktere clovek stoji, ne ji obejit. Z Mych hleda ve svych, z Nezarazenych ve fronte, z Vsech v celem rozsahu stropu, a vysledek se vraci do te same zalozky. Volba "hledat vsude" dava smysl jen tam, kde zalozka Vsechny vubec je. ### Overeno na bezici instanci | Kdo | Zalozka Vsechny | Sekce ve filtru | | ------------------------- | --------------- | --------------- | | spravce platformy | ano | vsechny | | Vomacka, vede Servicedesk | ano | jen Servicedesk | | Kriz, radovy clen | ne | zadne | A cely pohyb ticketu frontou: admin preda TK-4819 do sekce, Kriz ho vidi ve fronte, vezme si ho a objevi se mu v Mych ticketech. ## 2026-09-02 - Nezarazene vidi kazdy, a dlazdice "Moje tickety" Dve veci, ktere ze stropu viditelnosti vypadly. ### Nezarazeny ticket je ve stropu vzdycky Prvni verze ho schovavala: kdo nevidel na celou firmu, nevidel ticket, ktery nema resitele ani skupinu. Byla to diera v provozu. Prichozi ticket, ktery jeste nikdo nesmeroval, nepatri do zadne sekce, takze by ho nevidel nikdo krome vedeni - a nikdo by si ho nevzal. **Fronta je spolecna, prave proto je to fronta.** `withinVisibility` proto pousti kazdy ticket bez resitele, at uz ma skupinu nebo ne. Jakmile si ho nekdo vezme, plati strop jako u kazdeho jineho. ### Dlazdice "Moje tickety" a "Fronta bez resitele" Byly v navrhu od zacatku a nikdy se neudelaly. Presne jak navrh rikal, staci zaznam v katalogu a zdroj v `builtinSources` - **zadna nova komponenta**, data pocita tatáz cesta jako u vykonu resitelu. ```ts 'list.myTickets': { kind: 'ticketList', filter: { assignee: ['me'], closed: false }, limit: 8 }, 'list.unassigned': { kind: 'ticketList', filter: { assignee: ['unassigned'], closed: false }, limit: 8 }, ``` K tomu tri veci, ktere si to vyzadalo: - **`closed` ve `WidgetTicketFilter`.** Bez nej se vyrizene tickety nedaly odfiltrovat: stav je volny retezec a vyjmenovat vsechny podoby slova "hotovo" se neda. Prospeje to i vlastnim widgetum, dosud neslo postavit "otevrene tickety podle typu". - **`ResolvedScope.personId` se plni vzdycky**, ne jen u pohledu `mine`. Prehled se pta v pohledu `tenant`, takze filtr `me` nemel co dosadit a dlazdice vracela prazdno. Kdo je to "ja", na pohledu nezalezi. - **Kdo neni veden jako resitel, tomu se "Moje tickety" nenabidnou.** Ticket se prirazuje resiteli, ne uctu, takze by takovy clovek koukal na prazdno navzdy a nedozvedel se proc. ### Vychozi rozlozeni Nahore to, co clovek muze udelat, teprve pod tim cisla: ```text Moje tickety, Fronta bez resitele Aktivni automatizace, Otevrene tickety, Bezici incidenty Posledni tickety, Incidenty ``` Graf behu z vychozi sady ven. Je to nejmene srozumitelna dlazdice pro noveho cloveka - behy ceho a co s tim - a zabira celou sirku. V katalogu zustava. **Zmena se projevi jen tem, kdo si dashboard jeste neupravili.** Rozlozeni se uklada za dvojici uzivatel a firma, kdo uz si ho osahal, musi dlazdice pridat rucne nebo dat "Vychozi". ### Overeno na bezici instanci Kriz jako radovy clen sekce: "Moje tickety" vraci jeho TK-4820, "Fronta bez resitele" nezarazeny TK-4819, a v seznamu ticketu vidi oba. Pred opravou `personId` vracela prvni dlazdice prazdno. ## 2026-09-02 - Detail ticketu ma rozradovace, ne tri karty pod sebou Navrh v [25](25-navrh-pristupny-portal.md) mel detail jako jednu slozku s rozradovaci, ale v aplikaci zustaly tri karty pod sebou: obsah, prichozi udalosti a log prubehu. Detail byl tri obrazovky dlouhy a to podstatne, tedy obsah pozadavku, se v tom ztracelo. Ted jsou to listy jedne karty: | Rozradovac | Co je na nem | | ------------ | ------------------------------------------------- | | Požadavek | obsah a pole na komentar, vychozi | | Log průběhu | co delala automatizace a co ji sluzby vratily | | Události | co presne prislo zvenku | | Vlastní pole | hodnoty poli typu ticketu | U kazdeho je pocet, takze je videt, jestli se tam vubec vyplati kliknout. **Vlastni pole se nenabizeji, kdyz zadna nejsou** - prazdny rozradovac je jen dalsi vec k prehlednuti. Vychozi je vzdycky Pozadavek: to je duvod, proc clovek ticket otevrel. Log a udalosti jsou hloubkova data, na ta se sahá, az kdyz neco nesedi. Vlastni pole se do te doby nedala zobrazit vubec, i kdyz je ticket nesl - to je ten ctvrty rozradovac. Kresli je tatáz tabulka jako obsah ticketu (`DataTable` v `TicketBody.tsx`), takze se struktura vsude vypisuje stejne. Rozradovace pouzivaji **tentyz vzhled zalozek jako zbytek portalu**, ne tvar slozky z navrhu. Druhy styl zalozek na jedne obrazovce je presne ten drift, ktery jsme uklizeli u vstupnich poli. ## 2026-09-02 - Hierarchie firmy: kdo co vidi, a zalozka Firma Pohled na celou firmu dostal kazdy, kdo do ni patril: `scopes.push('tenant')` se v `access.ts` neptalo na nic. `mine` byl dobrovolny filtr, ne strop, takze resitel s roli `agent` poslal `?scope=tenant` a dostal cely provoz. Rozdil, na kterem to ted stoji: **pohled je co chci videt, strop je co vubec smim videt.** Existoval jen pohled a klientovi se veril. ### Model Priznak visi na **clenstvi**, ne na cloveku a ne na skupine. Diky tomu muze byt clovek v peti sekcich a jen ve dvou z nich videt vsechno - to role rict neumi, ta je jedna na celou firmu (`Membership` je dvojice firma a role). - `Membership.seesAllTenant` - vidi cely provoz firmy. Postaveni uctu ve firme, ne vlastnost resitele: clovek muze byt ve dvou firmach jednou reditel a jednou brigadnik. - `PersonGroup.members` je `{ personId, seesAll }` misto holeho `personIds`. `seesAll` je vedouci sekce. Vedoucich muze byt vic a jeden clovek muze vest vic sekci, proto to neni zvlastni pole na skupine. - Stara podoba `personIds` se dal cte, prevadi ji `groupMembers` - jedine misto, kde se to deje, aby skupina ulozena driv neprisla o cleny. ### Vypocet stropu `visibilityFor` v `data/access.ts`, sjednoceni ne prunik: ```text spravce platformy nebo seesAllTenant -> cela firma jinak -> moje tickety + vse ze sekci, kde mam zaskrtnuto + fronta techhle sekci ``` `seesAllTenant === undefined` jsou zaznamy ulozene driv. Tam rozhoduje pravo `ticket.assign.others`: kdo dosud smel prehazovat cizi praci, uz stejne cely provoz videl, takze se mu nic nebere. Bezny resitel timhle sitem neprojde a spadne na svoje sekce, coz je prave ta zmena, o kterou jde. ### Kde se to vynucuje **Strop je povinna soucast `TicketFilter`**, stejne jako `tenantIds`. Nepovinny filtr na prava je filtr, ktery jednou nekde chybi - takhle prekladac ukaze kazde misto, ktere ho jeste nema. Pri zavedeni jich naslo trinact. - `listTickets` filtruje pres `withinVisibility` - `getWorkload` a `getAgentStats` uz nesahaji do pole ticketu primo, jdou pres `listTickets`. Driv obchazely kazde omezeni viditelnosti - `getTicket` kontroluje strop i u jednoho ticketu. Bez toho by stacilo znat ID: seznam by ho schoval, ale adresa detailu vydala - detail a prevzeti pocitaji strop **za firmu ticketu**, ne za prave prepnutou - odkaz z pohledu "vse" muze vest do jine firmy uzivatele ### Helpdesk Ridi se toutez hierarchii, jen "moje" znamena neco jineho: zadavatel pozadavek nikdy nema prirazeny, resi ho nekdo u dodavatele. Proto ma ticket nove `createdById` a v helpdesku plati: kdo vidi celou firmu, vidi vsechny jeji pozadavky, ostatni jen ty svoje. ### Zalozka Firma Bylo to roztazene mezi dve mista. Nove je pod **Firma** vsechno, co se firmy tyka: Prehled, Resitele, Sekce, Role a prava, Typy ticketu, Pozvanky. V Nastaveni zustal Muj ucet a platformni veci, ktere klient nevidi. Role a Typy jsou ted samostatne komponenty (`RolesAdmin`, `TicketTypesAdmin`), ne kus stranky nastaveni. Sekce maji vlastni panel misto obecneho `EntityAdmin`: u kazdeho clenstvi je prepinac "vidi vse ze sekce" a radek na cloveka obecny editor s poli neumi. ### Overeno na bezici instanci Tri ucty nad tymiz daty: | Kdo | Vidi | | -------------------------- | --------------------------------------- | | spravce platformy | vsechny 3 tickety vcetne neprirazeneho | | Vomacka, vede Servicedesk | 2, tedy tickety obou clenu sekce | | Kriz, radovy clen tehoz | 1, jen svuj | K tomu: adresa cizho ticketu vraci 404, vytizeni tymu ukazuje jen viditelne, a po zaskrtnuti priznaku u clenstvi se rozsah zmeni hned. ### Co z toho plyne Neprirazeny ticket **bez skupiny** nevidi nikdo krome toho, kdo vidi celou firmu. Je to spravne podle modelu - takovy ticket nepatri do zadne sekce - ale znamena to, ze prichozi praci musi nekdo smerovat, jinak radovym clenum nikdy nedorazi. ## 2026-09-02 - Obsah ticketu se na detailu cte `body` je jeden retezec, ale co v nem stoji, urcuje ten, kdo ho naplnil. Krok "Zalozit nebo doplnit ticket" dosadi `{{data}}` a sablona objekt prevede na JSON, takze v obsahu skoncila slozena zavorka vypsana jako odstavec. Necitelne, i kdyz jsou v ni presne ty udaje, kvuli kterym ticket vznikl. Nova `web/src/components/dashboard/TicketBody.tsx` telo prectе: | Co v tele je | Jak se to ukaze | | ----------------------- | ------------------------------ | | veta od cloveka | text tak, jak prisel | | cely retezec je JSON | tabulka klic a hodnota | | seznam objektu | tabulka se sloupci podle klicu | | text a za nim JSON | odstavec a pod nim tabulka | | cokoliv, co se neprecte | text tak, jak prisel | Ctvrta radka je bezny pripad: sablona `{{voicebotId}}, {{data}}` da presne tohle. Deli se az od **prvni zavorky, od ktere je zbytek platny JSON** - bez te druhe podminky by veta s jednou slozenou zavorkou skoncila rozpulena. Zanoreni se kresli o uroven niz jako vnorena tabulka, hloubeji uz jako JSON: tabulka v tabulce v tabulce se necte a odesilatele umi zanorit data hluboko. Nic se nezahazuje, neprecteny obsah se ukaze jako puvodni text. Overeno na osmi tvarech tela vcetne toho, ktery na instanci opravdu vznika, vety se slozenou zavorkou a nedopsaneho JSONu. ## 2026-09-02 - U posledniho volani je videt, co presne prislo Prvni verze seznamu poslednich volani ukazovala cas a duvod odmitnuti, ale ne data. To je k nicemu: "parametr data ma mit typ text" nerekne, co tedy prislo, a bez toho zbyva hadat. Argument, ze se telo nema drzet kvuli zakaznickym datum, navic neobstal - tickety si cela prijata tela u udalosti drzi uz davno, takze si to jedno misto zakazovalo neco, co aplikace jinde bezne dela. Radek volani se ted rozklikne na dve veci: - **rozpad po parametrech**: jmeno, cesta, co se cekalo a co na te ceste opravdu bylo, vcetne nahledu hodnoty. Sklada ho `readPayload`, protoze jen tam je videt kontrakt i telo naraz - pozdeji uz se kontrakt muze zmenit - **cele prijate telo**, vcetne klicu, ktere odesilatel posila navic a kontrakt o nich nevi. Delsi nez 8 kB se usekne ```text ok callSid | cekame string, povinny | prislo text = CA-B CHYBA status | cekame string, povinny | prislo nepřišlo CHYBA data | cekame object | prislo text = tohle je text misto objektu ``` Telo se do zaznamu zmrazi na retezec, aby se pozdeji nezmenilo pod rukama, a serializace je v `try` - cyklicka struktura nesmi shodit prijem webhooku. ## 2026-09-02 - Kontrakt webhooku: objekt jde vybrat a odmitnuti je videt Parametr spoustece `data` byl deklarovany jako `type: string, required: true`, zatimco odesilatel ho posila jako objekt a v prvni zprave hovoru ho jeste nema. Kazde volani proto skoncilo na 400 a automatizace hodinu nedelala nic. Za tim byly tri veci, kazda sama o sobe malicherna: - **Rucne pridany parametr byl vychozi povinny**, zatimco parametr odvozeny z ukazkoveho tela nepovinny. Dve ruzna vychozi nastaveni pro tutez vec v jednom formulari. Nove je nepovinny i rucne pridany: povinny znamena "odmitni volani" a do toho nema nikdo spadnout omylem. - **Objekt a seznam neslo vybrat.** `declarableFieldTypes` nabizel jen `string`, `number`, `boolean` a `date`, a `TriggerConfig.tsx` mel jeste treti kopii toho seznamu. Deklarovat `data` jako objekt tedy neslo, i kdyz `matchesType` objekt umi a `operatorsByType` pro nej ma operatory. Ted jsou v nabidce oba a klient si vlastni kopii nedrzi. - **Odmitnuti nebylo nikde videt.** Skoncilo jako `console.warn` v logu kontejneru: zadna udalost, zadny beh, nic na detailu automatizace. Ten treti bod je ten podstatny. Chybu v kontraktu udela ten, kdo ho psal, ale **400 dostane odesilatel** - a ten s tim nic nenadela, casto je to cizi sluzba, ktera volani neopakuje. Majitel automatizace se nedozvi nic a v portalu vypada vsechno v poradku. Detail automatizace proto ukazuje **poslednich deset volani**: cas, jestli proslo nebo ne, a u odmitnutych duvod. Telo se schvalne neuklada, duvod uz rika, co je spatne, a drzet payloady by znamenalo mit v pameti kopie zakaznickych dat. Seznam je v pameti, restart ho zahodi - je to diagnostika posledni hodiny, ne historie. Incident se z toho nezaklada a upozorneni se neposila: staci radek, protoze implementator se ozve sam a tady je videt co. Vzorova automatizace ma `data` opravene na `type: 'object', required: false`. Overeno na bezici instanci s vlastnim DATA_DIR: telo s `data` jako objektem projde, prvni zprava hovoru s `data: null` projde, telo bez `callSid` se dal odmita - a vsechna tri jsou videt v seznamu poslednich volani. ## 2026-09-02 - Navrh: pristupny portal a viditelnost Novy [25-navrh-pristupny-portal.md](25-navrh-pristupny-portal.md). Je to navrh, ne popis stavu, nic z nej zatim neni naprogramovane. Vzniklo to z otazky, jak portal priblizit cloveku, ktery ho nikdy nevidel. Odpoved se rozpadla na pet veci, ktere spolu souvisi vic, nez to vypada: - **prehled ukazuje jen cisla, zadna slovesa.** Vsech sest widgetu ve vychozi sade jsou statistiky a seznamy. Navrh pridava "Moje tickety", "Fronta bez resitele", "Co potrebujete udelat" a "Zaciname" - **formulare nemaji spolecnou vrstvu.** `inputClass` je nadefinovany na 13 mistech a rozesel se do peti ruznych vzhledu, `Field` je napsany trikrat. V `components/ui/` neni zadny formularovy prvek - **hledani neumi to jedine, k cemu je.** Klientsky filtr nehleda v obsahu, ve vlastnich polich ani v externim ID, takze hovor podle `callSid` se dohledat neda. Navrh je modal s kriterii, protoze ticket nema pevnou sadu poli - **viditelnost ticketu se neda omezit.** Pohled `tenant` dostane kazdy, kdo do firmy patri, `mine` je dobrovolny filtr a ne strop. Navrh vede viditelnost pres clenstvi (priznak na firme, priznak u kazde skupiny), ne pres role - role jsou na celou firmu a neumi rict "v jedne sekci vidim vse, v druhe svoje" - **uloziste neprezije nasazeni.** Overeno v praxi: po dnesnim nasazeni zustaly ve firme dva tickety, predtim jich byly tisice Dve veci, ktere stoji za zapamatovani, i kdyby se navrh nikdy nedodelal: 1. **Strop viditelnosti nepatri do hledani, ale do cteni ticketu.** Cesty k ticketum jsou dnes tri (`listTickets`, `getWorkload`, `getAgentStats`) a dve z nich filtruji `tickets` primo. Kdyby strop resilo jen hledani, obejde se widgetem nebo souhrnem. 2. **Pohled a strop nejsou totez.** Pohled je co chci videt, strop je co vubec smim videt. Dnes existuje jen pohled a klientovi se veri. Ctyri otevrene otazky jsou v zaveru navrhu: co znamena "moje" pro strop, jak se strop potka s helpdeskem, jestli hledat i v udalostech a kdo smi viditelnost nastavovat. ## 2026-09-02 - Vzorova automatizace srovnana s bezici instanci `seedRealAutomations` v `src/data/automationStore.ts` drzelo starsi podobu stromu nez ta, ktera na instanci opravdu bezi. Protoze data neprezivaji redeploy, je tenhle seed jedine misto, kde nastaveni prezije nasazeni - a kdyz se rozejde, znamena to po kazdem nasazeni stavet strom rucne znovu. Opsano z bezici instance: spoustec ma sest parametru misto tri (pribylo `result`, `rating` a `data`, vsechny s cestou do `data`), krok "Zalozit nebo doplnit ticket" pise do obsahu `{{voicebotId}}, {{data}}` a stav bere z `{{result}}`, a za nim je podminka nad vysledkem: cokoliv krome "Chybějící informace" ticket zavre, jinak jde na servicedesk s vysokou prioritou. Pravidlo, ktere z toho plyne a je i v komentari u funkce: **kdyz se strom na instanci zmeni, patri ta zmena sem.** Jinak ji dalsi nasazeni zahodi. ## 2026-09-02 - Zalozit NEBO DOPLNIT ticket: doplneni konecne doplnuje Automatizace mela v kroku "Zalozit nebo doplnit ticket" pole Obsah nastavene na `{{rating}}`. Data v behu prokazatelne byla - v udalostech ticketu je hodnoceni videt cele - ale ticket zustal s prazdnym obsahem. ### Cim to bylo `intakeEvent` deli praci na **zalozeni** a **navazani na existujici ticket**. Vsechno z `create` platilo jen pro tu prvni vetev. U existujiciho ticketu se doplnovaly pouze vlastni pole a stitky. Predmet, obsah, typ, priorita, kanal, zakaznik, resitel ani skupina ne - tise se zahodily. U hovoru to znamena, ze obsah nedorazi nikdy. Prvni zprava jen oznami, ze hovor zacal (`status: in-progress`, `data: null`), a **prave ta ticket zaklada**, tedy s prazdnym obsahem. Hodnoceni prijde az posledni zpravou, kdy uz ticket existuje. Stav byl jedina vyjimka, protoze ho krok nastavuje zvlast pres `updateTicketStatus`. Proto fungoval a zbytek ne, a proto to vypadalo jako chyba jednoho pole. ### Jedno pravidlo misto dvou seznamu Zalozeni a doplneni ted delaji totez: > Neprazdna hodnota prepise, prazdna nemaze. `IntakeInput` ma na to `apply`, v `create` zustal jen zaloha predmetu a vychozi stav. Dva ruzne seznamy poli by se stejne zase rozesly a nekde by zas neco chybelo. Prazdna hodnota nemaze schvalne. Prave to byla puvodni obava, kvuli ktere se zapisovalo jen pri zalozeni: pozdejsi zprava bez jmena zakaznika je bezna a smazat kvuli ni jmeno by bylo horsi nez ho nedoplnit. Nove se prepise jen to, co odesilatel opravdu poslal, takze pojistka plati a data se neztraci. Dve vyjimky zustavaji: zaloha predmetu z externiho ID plati jen pri vzniku a stav chodi pres `updateTicketStatus`, ktere resi i priznak vyrizeni, cas vyreseni a pocet znovuotevreni. ### Data smi chodit po castech Vlastni pole typu se **scitaji podle klicu**. Prvni zprava posle `data`, druha `data2` a ticket ma obe. Odesilatel se nemusi predem dohodnout, co vsechno posle, a nemusi posilat vsechno pokazde. Prazdny retezec pritom pole nemaze. Sablona, ktera na nic neukazuje, se dosadi prazdnem, takze `{"vysledek":"{{result}}"}` u zpravy bez vysledku posilalo prazdno a **prepsalo tim hodnotu z minule zpravy**. Vymazat pole jde poslanim `null`: to uz je zamer, ne vedlejsi ucinek nevyplnene sablony. ### Stav se ted ulozi uz pri vzniku `create.status` se do `createTicket` vubec nepredaval, takze ticket vznikl s vychozim "Nový" a hned se prepsal. V logu pak stalo "stav Nový -> completed" u ticketu, ktery v "Nový" nikdy nebyl. Komentar u volajiciho tvrdil, ze uz je to opravene - opravena byla jen jedna strana. ### Faze dorazena do konce Faze byla zrusena uz driv, hodnoty z ni patri do stavu. V katalogu po ni ale zbyval krok "Posunout do dalsi faze" a pole Faze u zalozeni ticketu. Ani jedno nemelo co delat: ticket ani typ ticketu fazi nemaji. Krok by pri behu selhal na chybejicim skriptu a pole se tise zahazovalo, takze `{{status}}` napsany do Faze nedelal nic. Oboji je pryc. ### Aby bylo videt, ze se to ulozilo Krok v logu rekne, co doplnil: `doplnen TK-123, stav completed, obsah`. Driv radek jen oznamil, ze se ticket doplnil, a nebylo poznat cim - u dat, ktera dorazi az druhou zpravou, je to zrovna ta informace, kterou clovek hleda. ## 2026-08-28 - pad portalu uz nesmi shodit stranku a zaklada incident Ukazka tela webhooku shazovala cely builder pri psani cesty parametru. Chyba sama byla na jednom radku, ale to podstatne je, **ze jedna vyjimka pri vykreslovani odstranila celou stranku**. Uzivateli zustala bila plocha a rozdelana prace byla pryc. Aplikace na to nemela zadnou pojistku. ### Pojistka proti padu vykreslovani Nova `web/src/components/ErrorBoundary.tsx` obaluje obsah portalu. Mistni chyba ted shodi svoji cast obrazovky, ne aplikaci: navigace, prepinac firmy i odhlaseni zustanou funkcni a uzivatel ma kam odejit. Odchod na jinou stranku pojistku srovna zpatky. Je to jedina trida v celem klientovi. Hook na tohle neni a nebude, React to umi zachytit jen takhle. ### Pad zaklada incident `POST /api/dashboard/client-crash`. Bez toho je jedina stopa v konzoli prohlizece uzivatele, kam se nikdo nedostane - takze bychom o padu vedeli jen tehdy, kdyby ho nekdo nahlasil. To znamena o vetsine padu nevedet. Incident nese dve casti, stejne jako ostatni incidenty: - `title` a `impact` cte zakaznik, tedy zadne stack trace, - `detail` cte spravce platformy: hlaska, misto v kodu, strom komponent, adresa stranky, ucet, prohlizec a **verze buildu**. Verze buildu je tam schvalne. U tohohle padu se ukazalo, ze bez ni se neda poznat, jestli uzivatel vidi chybu, ktera uz je opravena, nebo novou. Tentyz pad na tomtez miste zalozi incident nejvys jednou za deset minut. Pad pri vykreslovani se opakuje pri kazdem prekresleni a jinak by z jedne chyby vzniklo padesat incidentu a ten pravy by v nich zapadl. ### Sama ukazka - Do cesty parametru se da zanorit vzdy. Kdyz na miste je skalar, nahradi se schrankou - driv se zapisovalo do retezce a to shodilo stranku. - Parametr, kterym vede cesta jineho parametru, uz nedostane zastupnou hodnotu podle typu. Plati vnitrni struktura: kdyz je jeden parametr `data` a druhy `data.result`, **`data` musi byt objekt**. Zastupna hodnota by ho prepsala. - Cela ukazka je navic v `try`. Je to napoveda a nesmi shodit ani ten kus obrazovky, at uz do ni prijde cokoliv - parametry se pisou znak po znaku a rozdelany stav je normalni. ## 2026-08-28 - MCP EasyWeb podle skutecne specifikace (auth v2) Predchozi verze posilala na `/login` jen jmeno, heslo a nazev zarizeni. Server na to odpovidal `400 Bad Request` na cokoliv, i na spravne udaje, protoze **cekal neco uplne jineho**. EasyWeb ma auth v2: token se nevydava proti uctu, ale proti **zarizeni**, a to je pár klicu ECDSA P-256. Jmeno a heslo se pouziji jedinkrat, kdyz se klic registruje, a soucasti registrace je podpis, kterym zarizeni dokazuje, ze privatni klic k poslanemu verejnemu opravdu ma. Od te chvile se podepisuje kazde volani, ktere s tokeny hybe. Overeno proti bezicimu serveru: se spravnym telem uz `/login` nevraci 400, ale 401 s neplatnymi udaji. Ucty z jejich testovaciho `settings.json` na verejnych instancich neplati, takze dal se bez skutecnych udaju nedostanu. ### Prihlaseni - `src/mcp/easyweb/crypto.ts` - klice, podpisy, otisky. Podpis musi byt **P1363**, tedy holé `r || s`, 64 bajtu. Node podepisuje ve vychozim nastaveni do DER a ten by protistrana neuznala. - `src/mcp/easyweb/device.ts` - klic zarizeni se vyrobi jednou a **prezije restart**: uklada se mezi udaje konektoru, ktere uz jsou zasifrovane. Pole ma novy priznak `managed`, takze ho ve formulari nikdo nevidi. - `src/mcp/easyweb/session.ts` - tri tokeny, retez s ustupy (platny pristupovy, obnova obnovovacim, obnova zarizenim, cele prihlaseni), **jedno prihlaseni naraz na konektor**, tokeny **jen v pameti**. Ta posledni tri pravidla nejsou opatrnost navic: tokeny jsou jednorazove, druhe pouziti server odmita kodem 409 a **umi zarizeni zablokovat**. Dve soubezne automatizace nad tymz napojenim by bez jedne sdilene rozdelane operace spustily dve obnovy a druha by pracovala se spotrebovanym tokenem. ### Transport - **Server si sam vybira, jestli odpovi JSON telem, nebo SSE streamem**, a streamem odpovida i na obycejna volani. Klient proto nabizi obojí a cte stream **po kouscich** - u dlouhych uloh ho server sam nezavira a posila do nej tlukot srdce, takze cekani na konec by skoncilo az timeoutem. - Handshake plati **na token**, ne na volani. Server drzi sezeni u tokenu. - Odmitnute sezeni prijde jako chyba `-32008` **uvnitr uspesne odpovedi**. Bez zvlastniho osetreni by to vypadalo jako chyba volani a krok by skoncil misto toho, aby se prihlasil znovu. - Seznamy se skladaji pres vsechny stranky. Bez toho je videt jen prvni, tedy asi dvacet nastroju, a vypada to jako uplny seznam. - Odpoved se rozbaluje rekurzivne (`structuredContent`, `contents`, `content`, JSON zapsany jako text). Jinak by v kroku skoncil JSON jako retezec, se kterym uz se v podmince nic nesvede. ### Dlouho bezici nastroje Nastroj, ktery server odmitne spustit rovnou, se spusti jako **uloha** a ceka se na ni dotazovanim. Stav z `tasks/list` a `tasks/get` je to jedine, co je vzdycky pravda - notifikace o prubehu chodi nejvys jednou. Dokoncena uloha ze seznamu mizi, takze po nekolika marnych kolech se prejde na primy dotaz. Limit kroku se pri tom posouva z patnacti sekund na deset minut. Kdyz uloha nedobehne ani tak, neni to chyba: na serveru bezi dal a krok to rekne. ### Co z toho zbylo nedodelane Trvaly kanal notifikaci (GET SSE), nahravani souboru po castech a hlidani zmen kontraktu podle verze serveru. Vsechno je v [24-mcp-konektory.md](24-mcp-konektory.md) v seznamu toho, co chybi. ## 2026-08-28 - dve MCP sluzby: obecna a EasyWeb, strankovani nastroju MCP je standard, ale **prihlaseni k nemu ne**. Oficialni specifikace stoji na OAuth 2.1 a objevovani autorizacniho serveru pres `.well-known`. EasyWeb (Centaur) ma prihlaseni vlastni: `POST /login` s HTTP Basic vrati trojici tokenu a obnovuje se vlastnimi endpointy. Zadny OAuth, zadne `.well-known`, jina verze protokolu, zadne SSE ani hlavicka sezeni. Proto jsou v katalogu **dve sluzby**, ne jedna s prepinacem: firma pri zakladani konektoru vyplnuje neco jineho. U obecne ID a tajemstvi aplikace nebo hotovy token, u EasyWebu jmeno, heslo a nazev zarizeni. Slucovat to by znamenalo formular, kde je pulka poli vzdycky k nicemu, a hadani, ktera pulka to je. Obecna sluzba pritom **zustava plnohodnotna**. Vlastni server je duvod pridat sluzbu, ne duvod zavrit dvere ostatnim. ### Pribylo - `src/mcp/dialect.ts` - rozdily obou serveru na jednom miste: prihlaseni, verze protokolu, jestli se prijima SSE a jestli se posila `Mcp-Session-Id`. Rozesete po klientovi by u kazdeho dalsiho serveru pribyl dalsi `if` jinde. - **Sluzba MCP EasyWeb.** Adresa, jmeno, heslo, nazev a otisk zarizeni. Prihlasovaci adresy si portal odvodi z adresy serveru sam. Otisk se doplnuje z ID konektoru, aby server poznal totez zarizeni. - **Hotovy token u obecne sluzby.** Rada verejnych serveru nic jineho nenabizi a bez toho by na ne neslo zalozit konektor. - **Objevovani pres `WWW-Authenticate`.** Specifikace to ma jako povinnou cestu: server u odpovedi 401 rekne, kde jsou jeho metadata. Pouziva se az kdyz obvykla mista selzou, protoze to stoji volani navic. - **Zivotnost z tela tokenu.** Kdyz server `expires_in` ani datum neposle, cte se `exp` z JWT. To je presne pripad EasyWebu. - **Strankovani nastroju.** Nastroj s parametrem `cursor` dostane v builderu prepinac Nacist vsechny stranky. Kurzor je hodnota z odpovedi, takze v dobe stavby stromu ho nikdo nezna a nejde ho vyplnit dopredu. Krok pak vraci navic `items`, `pages`, `pageCount` a `truncated`. Strop je 20 stranek. ### Opraveno Prihlaseni driv zkousela `password` grant a HTTP Basic proti hlavnimu endpointu. Prvni OAuth 2.1 zrusil, druhe neni nikde ve specifikaci a u EasyWebu by stejne neproslo - ten chce Basic na `/login`, ne na `/mcp`. ## 2026-08-28 - MCP: prihlaseni jmenem a heslem, tokeny si resi portal Predchozi verze chtela po uzivateli token. To bylo spatne zadani: zakaznik dostane ke svemu serveru **adresu, jmeno a heslo**, token nedostane a nema jak ho ziskat - vyda ho az autorizacni server a ma omezenou zivotnost. Novy `src/mcp/auth.ts` resi cely zivotni cyklus: - **Kde se prihlasit** se zjisti od serveru pres `/.well-known/oauth-protected-resource` a metadata autorizacniho serveru. Rucni pole na adresu je jen zaloha pro servery, ktere metadata nemaji. - **Cim** se zkusi v poradi `client_credentials` a `password`. Ktere z toho firma dostala, se z udaju samych poznat neda a nutit ji to vybirat by znamenalo ptat se na neco, co nevi. - **Bez autorizacniho serveru** se posle HTTP Basic. Mensi servery zadny OAuth nemaji a jmeno s heslem je u nich presne tohle. - **Zivotnost urcuje server.** Token se vymeni minutu pred vyprsenim, obnovi se pres `refresh_token`, kdyz ho server vydal, a kdyz `expires_in` chybi, pocita se s peti minutami - tedy odhaduje se dolu. - **Odmitnuty token** (401 na token, ktery jsme meli za platny) se zahodi a volani se zopakuje jednou. Podruhe uz ne, to uz nejsou platne udaje. Token se drzi **jen v pameti**. Je kratkodoby, po restartu se o novy rekne znovu, a ulozit ho by znamenalo vsechna rizika ulozeni bez jakekoliv vyhody. Kes si drzi otisk udaju, takze zmena hesla ulozeny token zneplatni. Zpusob prihlaseni je videt v hlasce u konektoru: uzivatel vyplnil jmeno a heslo a ma vedet, jak s nimi portal nalozil, nez zacne hledat chybu jinde. ## 2026-08-28 - MCP konektory hotove Firma si zalozi napojeni na svuj MCP server, stiskne **Nacist nastroje** a jeho nastroje se objevi v builderu jako kroky. Popis v [24-mcp-konektory.md](24-mcp-konektory.md). Navrh byl predtim dvakrat prepsany a stoji za to rict proc. Prvni verze davala nastroje do vyberu kroku, ale popisovala MCP jako obycejny katalog vzdalenych procedur - tak nedava nic navic proti HTTP konektoru. Druha verze to prehnala opacnym smerem a chtela nastroje predavat jen modelu. Vysledne zadani je uprostred: **klientem jsme my**, nastroj vybira clovek v builderu, a prinos je v tom, ze napojeni na cizi system vznikne bez radky kodu u nas. ### Pribylo - **Sluzba `mcp`.** Jedina v katalogu, ktera nema zadne pevne operace - rekne je az server. Udaje jsou adresa serveru, token nebo klic v `X-API-Key`. - **`POST /connectors/:id/mcp/tools`.** Zepta se serveru na `tools/list` a ulozi vysledek. Je to zaroven overeni konektoru, proto tlacitko "Overit" u MCP neni. - **Klient protokolu** (`src/mcp/client.ts`): handshake, sezeni z hlavicky odpovedi, odpoved jako JSON i jako SSE stream, strankovani nastroju. - **Prevod schemat** (`src/mcp/schema.ts`). Ze schematu vzniknou pole kroku a zpatky se z vyplnenych retezcu udelaji argumenty ve spravnych typech. Ten druhy smer je ten podstatny: server ceka `{"limit": 10}`, ne `{"limit": "10"}`, a rada serveru na tom spadne az uvnitr nastroje. - **Nastroje u konektoru** (sloupec `mcp`, migrace `004`). Bez ulozeni by po restartu zmizely z katalogu kroky, ktere uzivatel uz ma ve stromech. - **Vnitrni krok `runMcpTool`.** Jedna obsluha pro vsechny nastroje vsech serveru - ktery to je, rika az ID operace `tool::`. ### Rozhodnuti, ktera stoji za zminku - **Nastroj patri firme, ne katalogu.** `serviceCatalog(tenantId)` bez firmy nevrati zadny nastroj. Zapomenuty argument tak znamena "nic", ne "vsechno" - stejne pravidlo jako u filtru na firmu. - **ID operace nese ID konektoru.** Firma muze mit dva servery a na obou nastroj `search`. - **Krok se neopakuje.** MCP nema idempotencni klic, druhy pokus po timeoutu by nastroj provedl podruhe. - **Chyba nemaze nastroje.** Vypadek serveru nesmi vymazat kroky z hotovych automatizaci. Prazdny seznam se naopak ulozi. - **Servery se pri startu neobvolavaji.** Jeden nedostupny by shodil katalog vsem. ### Opraveno mimochodem - `setStatus` v `connectors/postgres.ts` melo v `RETURNING` doslovny retezec `${COLUMNS}` misto dosazeni. V rezimu `file`, ve kterem bezi nasazeni, se to neprojevilo. - Dva odstavce v [12-sluzby-a-konektory.md](12-sluzby-a-konektory.md) byly dvakrat. ## 2026-08-28 - dokument pro programatory Novy [00-pro-programatory.md](00-pro-programatory.md): rozcestnik, model ctyr pojmu (sluzba, skript, konektor, krok), pravidla, ktera plati vsude, tri cesty vzniku operace, tri rezimy uloziste a seznam mist, ktera jsou krehka. Neni to kopie [03-architektura-a-mapa-kodu.md](03-architektura-a-mapa-kodu.md). Ta rika, kde co lezi, tenhle rika proc to tak je. ## 2026-08-28 - transformace maji svou kategorii, pribylo XML ### Odstraneno - **Sluzba AI zpracovani textu.** Delala totez co OpenAI, jen jinymi slovy. Dva zpusoby, jak udelat jednu vec, znamenaji jen otazku, ktery pouzit. Ukazkove stromy a stopy, ktere na ni odkazovaly, ted miri na `openai.chat`. ### Pridano - **Kategorie Transformace dat.** Driv byly mezi obecnymi sluzbami vedle webhooku a pauzy. Ve chvili, kdy jich je vic nez tri, je clovek hleda jako skupinu, ne mezi spoustecemi. - **Skript `transform.to-xml`.** Zapise objekt jako XML vcetne escapovani. Cizi systemy si XML porad rikaji casto (ISDOC, EDI, banky, statni sprava) a bez tohohle se sestavuje retezcem v HTTP kroku, kde se na escapovani zapomene pokazde. Klice, ktere nejsou platny nazev prvku, se prejmenuji a rekne se to nahlas. Klic zacinajici cislici dostane podtrzitko misto odriznuti - `2024` a `2025` by jinak byly totez a v dokumentu by se srazily. - **Navrh cele skupiny transformaci** v [13-transformace-dat.md](13-transformace-dat.md): formaty, pole a hodnoty, seznamy, text a kontrola. Vcetne toho, co v seznamu zamerne neni a proc. ## 2026-08-28 - AI zpracovani textu funguje Sluzba byla v katalogu bez pristupovych udaju a s `appId: 'ai-text'`, tedy mirila na aplikaci, ktera neexistuje. Konektor nemel co vyplnit a krok nemel co vykonat. ### Zmeneno - **Sluzba ma API klic a skutecnou adresu.** Stoji na `api.openai.com/v1` stejne jako OpenAI, overeni je `GET /models`. Model se bere z konektoru (`ctx.config.model`), aby se nevyplnoval u kazdeho kroku znovu. ### Pridano - **Tri skripty**: `ai-text.classify`, `ai-text.extract`, `ai-text.generate`. Proc to neni jedna sluzba s OpenAI: tam se posila volny dotaz a vraci volny text. Tady je uloha pevna a **vystup taky** - `category` a `confidence`, objekt s popsanymi poli, text s poctem slov. Prave na to jde ve strome navazat podminkou, na volny text ne. ### Zjisteno u toho - **Overeni konektoru projde i u sluzby, ktera neexistuje.** Proxy vraci na neznamou aplikaci `200` s prazdnym telem, a sluzba bez `verifyPath` se overuje prave pres `/health`. Konektor tedy rekne "Sluzba odpovida" i tam, kde nic nebezi. Tyka se to E-shopu, WhatsAppu, Facebooku, Instagramu, SMS a Slacku. Zapsane v [21-realne-sluzby.md](21-realne-sluzby.md), zatim neopravene. ## 2026-08-26 - jazyky, skutecny snimek v heru a opravene titulky ### Pridano - **Prepinani jazyku** (`web/src/i18n/`). Vybrany jazyk je jedna hodnota pro celou aplikaci, drzena mimo React - tentyz vzorec jako u vybrane firmy. Volba prezije obnoveni stranky a prepisuje ``. Anglicky slovnik je `Partial`: co v nem neni, spadne na cestinu, takze preklad jde doplnovat po castech misto jednoho velkeho skoku. Podrobnosti v [23-jazyky.md](23-jazyky.md). - **Prepinac jazyka** v hlavicce a v mobilnim menu. ### Zmeneno - **Snimek v heru vypada jako portal doopravdy.** Byl svetly a ukazoval obrazovku, ktera v produktu neexistuje. Ted ma tmavy podklad, tytez zalozky vcetne prepinace firmy, tytez sloupce tabulky i tytez odznaky stavu jako stranka Tickety. Kdyz se navstevnik prihlasi a uvidi neco jineho, prvni dojem z produktu je, ze web lhal. - **Jmeno znacky si sklada `usePageMeta`, ne stranky.** Bylo napsane natvrdo ve dvaceti ctyrech titulcich, takze prejmenovani produktu zmenilo `brand.ts` a titulky ne - hlavicka rikala nove jmeno a zalozka v prohlizeci stare. ### Opraveno - **Odkaz "Prohlednout si to" vedl mimo proxy.** Byl to ``, tedy koren domeny, ne `/apps//sluzby`. Ted je to `Link` z routeru jako vsude jinde. Byl to jediny takovy odkaz v celem webu. ## 2026-08-26 - na aplikaci je videt, ktery build to je Nasazena aplikace o dva commity pozadu funguje a vypada skoro stejne jako nova. Bez oznaceni buildu se to nepozna a hleda se chyba tam, kde zadna neni. ### Pridano - **Oznaceni buildu v pate postranniho menu**, pod tlacitkem Odhlasit se: verze, cas buildu a cislo commitu. Text jde oznacit jednim kliknutim, aby se dal poslat dal. - **Totez jako `` v hlavicce stranky.** V JS balicku uz to je, ale ten se musi stahnout a rozbalit. Meta znacka je videt na jeden `curl` bez prihlaseni - a prave to clovek potrebuje, kdyz zjistuje, co bezi na produkci. - Hodnoty dosazuje Vite pri buildu (`define` a plugin `build-stamp` ve `vite.config.ts`), takze neplati pro repozitar, ale pro ten konkretni balicek. ### Zmeneno - **`.dockerignore` uz nevylucuje `.git/`.** Build z nej cte cislo commitu; bez nej by se poznal jen cas buildu, ne co v nem je. Do vysledneho image se `.git` nedostane, ten se sklada z ciste zakladni image. ## 2026-08-26 - rebrand na WorkNuke Prevzeti vizualniho smeru z predlohy. Popis, jak znacka funguje v kodu, je v [22-znacka-a-design.md](22-znacka-a-design.md). ### Zmeneno - **Barevnost.** Akcent z cyanove na zelenou `#B7FF3C`, podklad z modrocerne na zelenocernou `#0F1411`. Web i portal stoji na tychz tokenech, takze se to projevilo naraz na obojim. - **Druhy akcent uz neni barva.** `accent-*` ukazuje do teze zelene rady jako `brand-*`. Klice zustaly, protoze je pouziva 341 mist; prepsat je vsechny naraz by byla zbytecne velka zmena. Totez u utility `text-gradient`, ktera uz negeneruje prechod, ale plnou zelenou. - **Stav "v poradku" je modrozeleny.** Akcent je zeleny, a dve zelene na jedne obrazovce, kazda o necem jinem, nikdo nerozlisi. - **Pismo.** Archivo na nadpisy, IBM Plex Sans na text, IBM Plex Mono na cisla a ID. Nadpisy maji tesny proklad, ktery Inter neumel bez trikareni. - **Znak.** Plocha bomba misto pismene v gradientnim ctverci. `LogoMark` je samostatny export, aby sel pouzit i uvnitr snimku v heru. - **Tlacitko.** Plna zelena misto prechodu ze dvou barev. Prave tim vystupuje z rady. - **Hero.** Prokladany nadtitulek misto odznaku, dvouslovne radky nadpisu, jedno primarni tlacitko plus textovy odkaz, ctyri slovesa misto tri odrazek a snimek portalu vybihajici z prave hrany. - **Navigace** je pri levem okraji a bez pilulky za aktivni polozkou. Jedina pilulka na liste je primarni akce. - **Nazev, claim a kontakt** v `brand.ts`. ### Opraveno u toho - **Zare v heru je pozadi sekce, ne vrstva pod ni.** Prekryv se zapornym z-indexem se schoval za podklad stranky a nebyl videt vubec. ### Co se zamerne nezmenilo Klice v prohlizeci (`automia.token`, `automia.tenant`), ID firmy `tnt_automia`, ID aplikace `csbot-prototype` a prihlasovaci e-maily v ukazkovych datech. Vypadaji jako jmeno, ale jsou to identifikatory. Duvody jsou v [22-znacka-a-design.md](22-znacka-a-design.md). ## 2026-08-26 - prepinac firmy: do sidebaru a prekresli obsah cely Dodelavka predchoziho bodu. Tvrzeni "prepinac plati pro cely system" nebylo pravdive: `useApiQuery` se na zmenu firmy zeptat umel, ale komponenty, ktere volaji `apiFetch` primo - `EntityAdmin`, a tim cele Nastaveni a zalozky Resitele a Skupiny v Lidech - o zmene nevedely a zustala na nich data predchozi firmy. ### Opraveno - **Obsah dashboardu ma `key` podle vybrane firmy**, takze prepnuti prestavi cely strom komponent. Zadny dotaz nemuze zustat stary, protoze zadna stara komponenta nezustane. Doplnovat zavislost do kazde komponenty zvlast je totez zapomenuti, jen o patro niz. ### Zmeneno - **Prepinac je v sidebaru nad navigaci**, ne v hlavicce. Vypada jako ovladaci prvek: ram, ikona firmy, sipka. Predchozi verze mela pruhledny ram a splyvala s popiskem - prepinac, ktery neni videt, je k nicemu. - **V hlavicce zustal jen nazev aktivni firmy.** Driv tam stalo natvrdo `tenants[0]`, takze po prepnuti ukazovala porad prvni firmu ze seznamu. - Kdo patri do jedne firmy, vidi misto prepinace jeji nazev. Vyber z jedne moznosti neni vyber. ## 2026-08-26 - prava plati za firmu, ne za cloveka `permissionsOf(user)` scitalo role pres vsechna clenstvi. Kdo byl spravce v jedne firme, jednal jako spravce ve vsech, kam patril. Uzivatel ve dvou firmach je vzacny, dusledek ne - to neni edge case, to je diera v pravech. ### Zmeneno - **Firma je povinny argument** u `permissionsOf`, `hasPermission` i `hasAnyPermission`. Zamerne povinny: kdyby byl nepovinny, prvni volani bez nej by tise vratilo vsechna prava. Prekladac tim rovnou nasel vsech ctrnact mist, ktera se ptaji. - **Kazde misto se pta za tu spravnou firmu.** Akce nad ticketem za firmu toho ticketu, uprava zaznamu za firmu toho zaznamu, zalozky a helpdesk za firmu, kterou ma clovek prepnutou. Kdo edituje zaznam jine firmy, musi mit pravo tam, ne tam, kde je zrovna prepnuty. - **Cizi firemni role se ignoruje**, i kdyby na ni clenstvi odkazovalo. Jinak by stacilo pripsat si ji do clenstvi v jine firme a prava by se prenesla. - **Cache prav ma v klici firmu.** Bez toho by prvni dotaz odpovedel i na druhou. - **`readScope` cte z jedne firmy, te prepnute**, ne ze vsech, kam clovek patri. V nastaveni se driv michaly typy ticketu a role napric firmami bez ohledu na vyber - stejna vada jako u prepinace, jen na jinem miste. ### Opraveno - **Systemove role se pri startu srovnaji s kodem** (`syncSystemRoles`). Vychozi sada se pouzije jen do prazdneho uloziste, takze nove pravo v katalogu se k uz bezici instalaci nikdy nedostalo - `ticket.create` a obe prava helpdesku by spravci firmy nikdy nenabehla a zalozka by se neobjevila. Meni se jen prava, jen u nasich roli, nazvu se to netyka. ## 2026-08-26 - prepinac firmy plati pro cely portal Prepinac byl stav uvnitr stranky Prehled. Prepnuti na LogiTrans zmenilo prehled a nic jineho: ostatni stranky volaly API bez `tenantId` a server sahnul po vychozi firme, takze v Lidech zustali lide Automie. To neni nepohodli, to jsou cizi data pod hlavickou jine firmy. ### Zmeneno - **Vybrana firma je jedna hodnota pro cely dashboard** (`web/src/lib/tenant.ts`), ne stav stranky. Prezije obnoveni stranky. - **`tenantId` doplnuje `apiFetch`**, ne jednotlive stranky. Kdyz si to mela pridavat kazda stranka sama, vetsina na to zapomnela - a zapomnetlivost byla prave ta chyba. Nedoplnuje se, kdyz cesta uz firmu nese (proklik z widgetu), u `scope=all` a mimo `/api/dashboard`. - **`useApiQuery` se pri prepnuti zepta znovu.** Bez toho by na strance zustala cisla predchozi firmy, dokud by ji nekdo neobnovil. - **Prepinac je v hlavicce nad obsahem.** Driv byl v Prehledu, ted plati viditelne pro vsechno. Nahradil i text, ktery ukazoval natvrdo prvni firmu ze seznamu bez ohledu na to, co bylo vybrane. - **Ulozena firma, do ktere uzivatel uz nepatri, spadne na vychozi.** Jinak by po odchodu z firmy koncil kazdy dotaz na 403. ### Zjisteno u toho - **Prava se scitaji pres vsechny firmy**, neprepocitavaji se podle vybrane. Kdo je spravce v jedne firme a resitel v druhe, ma po prepnuti porad prava spravce. Data se tim neprolomi, server je filtruje dal podle firmy, ale tlacitka ukazuji vic, nez by mela. Zapsane v [07-firmy-a-prava.md](07-firmy-a-prava.md), sekce Co chybi. ## 2026-08-26 - helpdesk: pozadavek, ktery vidi zadavatel i resitel Zakaznik nemel jak poslat pozadavek. Ticket pritom patri jedne firme, kdezto u helpdesku figuruji dve - ta, ktera se pta, a ta, ktera to resi. ### Pridano - **Pole `Ticket.helpdeskSourceId`** s firmou, ktera pozadavek poslala. Vlastnikem (`tenantId`) zustava ta, ktera ho **resi**. Zamerne tak: kdyby byl vlastnikem zadavatel, mel by resitel pozadavek jen jako cizi ticket a nemel by ho ve sve fronte, ve statistikach ani v prirazovani. Takhle je to na jeho strane obycejny ticket a nemuselo se sahnout na nic, co uz funguje. - **`Tenant.helpdeskProviderId`**, tedy komu firma posila pozadavky. Nastavuje spravce platformy v Nastaveni, Firmy. Kdo koho obsluhuje je obchodni vztah, ne volba klienta - kdyby si dodavatele vybiral uzivatel, poslal by pozadavek nekomu, s kym nema smlouvu. Bez vyplneneho dodavatele se pozadavek nezalozi a rekne se to nahlas. - **Sekce Helpdesk** (`/dashboard/helpdesk`) a router `/api/dashboard/helpdesk`: seznam vlastnich pozadavku, zalozeni, detail s prubehem a komentar. Zamerne to **neni druhy seznam ticketu** - chybi tu filtry, prirazovani i fronta, protoze zadavatele nezajima, kdo to ma u sebe. - **Prava `helpdesk.view` a `helpdesk.create`.** Prideluje je admin te firmy pres role, stejne jako u ostatnich prav. ### Zmeneno - **`listTickets` umi filtrovat podle zdroje** (`helpdeskSourceIds`). Vyplnene **nahrazuje** filtr podle vlastnika, protoze zadavatel vlastnikem neni. Bezny seznam ticketu se nezmenil. - **`getTicket` a `addComment` maji druhou cestu dovnitr.** Firma, ktera pozadavek poslala, ho smi cist a pripsat k nemu komentar, i kdyz ho nevlastni. Komentar je jedina zmena, kterou nad nim smi: stav, resitele a typ urcuje ten, kdo to resi. ## 2026-08-26 - portal prestal ukazovat provozni veci a ticket jde zalozit rucne Sada oprav podle toho, co v portalu drhlo. ### Zmeneno - **Hlasky o ulozisti a o odchozi IP adrese vidi jen spravce platformy.** Kam se uklada a z jake adresy volame ven resime my, ne zakaznik. "Data se ukladaji do souboru na serveru" na nej navic pusobi jako priznani, ze mu tu praci muzeme ztratit. Nemazou se, jen se schovavaji - nam poradi porad. Totez plati pro radek "volano z IP" v historii overeni. - **Typ ticketu se v automatizaci vybira ze seznamu.** Bylo to textove pole, do ktereho mel clovek opsat ID typu odjinud. Novy druh pole `lookup` je **ciselnik a volny text zaroven**: bud se vybere ze seznamu typu te firmy, nebo se hodnota dosadi z dat (`{{data.typ}}`). Typy ticketu jsou vlastnost firmy, takze nabidka chodi za tu, ve ktere clovek je. - **Stav ticketu je otevreny naseptavac, ne ciselnik.** Stav je volny retezec a vzdycky byl - ticket muze prijit z cizi aplikace s jejim vlastnim stavem. Vyber ze seznamu tomu odporoval. Ted je to textove pole s nabidkou toho, co firma uz pouziva (`GET /api/dashboard/tickets/statuses`), a nova hodnota projde stejne dobre. Zapisuje se az pri opusteni pole, ne po kazdem pismenu. - **"Kanal" se prejmenoval na "Odkud pozadavek prisel"** a rika o sobe, ze je nepovinny a slouzi jen k filtrovani a ikone v seznamu. - **Vstupni parametry u webhooku jsou oznacene jako nepovinne.** Webhook prijme cokoliv; rucne vypsany seznam parametru neni podminka, ale pohodli pro podminky - a vyplni se sam z vlepene ukazky tela. Prazdny seznam uz nehlasi, ze "bez nich nelze pridat podminku". ### Pridano - **Zalozeni ticketu rucne** (`POST /api/dashboard/tickets`, pravo `ticket.create`, tlacitko Novy ticket na strance Tickety). Dosud ticket vznikal jen z automatizace nebo z prichozi udalosti, takze pozadavek prijaty telefonem nemel jak do systemu. - **Zakaznik u ticketu je nepovinny.** Rucne zalozeny ticket je casto ukol, ne pozadavek od nekoho zvenku; povinna firma a kontakt by znamenaly, ze si je clovek vymysli. Ve formulari je zakaznik schovany pod odkazem. ### Co z teze davky jeste neni - **Sekce Helpdesk** pro zakaznika vcetne sdileni ticketu mezi admin firmou a tim, kdo ho zalozil. - **Prepinac firmy nad celym dashboardem.** Dnes je to stav uvnitr stranky Prehled, takze se prepnuti neprojevi v Lidech ani jinde - ostatni stranky volaji API bez `tenantId` a server pouzije vychozi firmu. Ma to doplnovat API vrstva na jednom miste, ne kazda stranka zvlast. ## 2026-08-26 - odesilani e-mailu ze schranky firmy Sluzba E-mail byla v katalogu jako popis: bez udaju, bez vykonne casti. Ted se odesila doopravdy, ze schranky, kterou si firma vyplni v konektoru. ### Pridano - **Sluzba E-mail pres SMTP.** V konektoru server, port, sifrovani, uzivatel, heslo, adresa a jmeno odesilatele a adresa pro odpovedi. Zadne udaje v prostredi - kazda firma odesila ze sve schranky. - **Vnitrni krok `email/send`.** SMTP neni HTTP a skript umi jen `ctx.http`, takze operaci vykonava vnitrni krok stejne jako zalozeni ticketu. Pristupove udaje pritom zustavaji v konektoru. - **Druh pole `html`.** Telo zpravy se pise jako HTML a builder ho vykresli jako vysoke pole s neproporcionalnim pismem. Runtime v nem **escapuje dosazene hodnoty**: znacky autora sablony jsou zamer, ostre zavorky v hodnote od zakaznika ne. Bez toho by text ticketu s `` prepsal rozvrzeni zpravy a `