# 09 - Navrh rozsireni Navrh, ne popis stavu. Nic z toho zatim neni naprogramovane. Kdyz se cast udela, prepise se do prislusneho souboru dokumentace a odsud zmizi. Vychazi z toho, co uz v aplikaci je. Popis stavajiciho stavu je v [01-prehled-a-stav.md](01-prehled-a-stav.md). ## Jedno rozhodnuti nad vsim ostatnim **Nezakladat druhy system akci.** Vsechno, co ma tlacitko na ticketu udelat, uz v katalogu konektoru existuje jako akce: zaloz doklad, posli e-mail, zapis do skladu, prirad cloveka. Ma to `inputs`, sablony `{{parametr}}`, deklarovane `outputFields` a validaci. Akce ma proto **vlastni definici, ale stoji na tom samem zakladu**: tentyz katalog konektoru, tentyz model kroku, tentyz builder, tataz validace, tentyz log. Kdyby se pro tlacitka postavil samostatny mechanismus, existovaly by dva zpusoby, jak zavolat Fakturoid, dva zpusoby, jak dosadit sablonu, a dva logy. Jeden by se casem opravoval a druhy ne. Stejne pravidlo plati na widgety (katalog je zdroj pravdy) a na prava (`access.ts` je jedine misto). ### Dve oddelene veci, ne jedna spolecna Automatizacni stromy a akce jsou **dve samostatne veci** a nemaji spolecnou vrstvu mezi sebou: | Vec | Spousti | Kde je definovana | | -------------------- | --------------------------- | ------------------------ | | Automatizacni strom | udalost, spoustec | seznam automatizaci | | Akce | clovek kliknutim na ticketu | seznam akci za firmu | **Akce se vaze na typ nebo tag ticketu.** Priklad: "Odeslat do iDokladu" pro objednavku. Na ticketu se pak vykresli CTA vsech akci, ktere na jeho typ nebo tag sedi a na ktere ma uzivatel pravo. **Automatizacni strom si to dela sam.** Nevola akci a nesdili s ni nic. Kdyz ma po prijeti objednavky vzniknout doklad v iDokladu, ma strom ten krok v sobe. Akce si svoje telo drzi sama. Muze to byt jeden krok konektoru, **vlastni strom**, nebo **vlastni skript** - podrobne nize u definice akce. Podstatne je, ze strom akce je jeji vlastni, ne odkaz nekam jinam. Spolecne je pod obojim jen dvoje, a v tom je ta uspora: **katalog konektoru** a **model kroku**. Operace "vytvorit doklad" je napsana jednou a obe strany na ni odkazuji. Strom akce pouziva tentyz `FlowStep`, tentyz builder a tutez validaci jako automatizace. Duplikuje se tedy nastaveni, ne logika, a nevznika druhy builder, ktery by zaostaval. Prvni verze tohoto navrhu mezi to vkladala treti entitu, sdileny postup volany z obou stran. Bylo to zbytecne: prinesla by verzovani napric, hloubku volani a jeden seznam navic, a to vsechno kvuli tomu, aby se nedvakrat vyplnilo mapovani poli. Zbyva z toho jedna vedoma cena, kterou je potreba znat: **kdyz se ma tataz vec dit automaticky i rucne, nastavuje se na dvou mistech.** Zmena v iDokladu znamena upravit strom automatizace a upravit akci. Kdyby to nekdy zacalo bolet, nejlevnejsi zaplata je akce, kterou umi zavolat i krok stromu - ale az kdyz to bude potreba, ne dopredu. ## 1 a 2 - Typy ticketu a akce nad nimi Body 1 a 2 jsou jedna vec. Bez typu ticketu neni na cem tlacitka rozlisovat, bez vlastnich poli neni s cim pracovat: akce "Potvrdit objednavku" potrebuje cislo objednavky, ne jen predmet a text. ### Typ ticketu Novy soubor `src/data/ticketTypes.ts`. Typ je nastavitelny za firmu, ne pevny v kodu. ```ts interface TicketTypeField { id: string; // 'fld_order_no', stabilni - odkazuji se na nej podminky key: string; // 'orderNumber', pouziva se v sablonach: {{orderNumber}} label: string; // 'Cislo objednavky', to co vidi uzivatel type: FieldType; // uz existuje v conditions.ts required: boolean; options?: Array<{ value: string; label: string }>; } interface TicketType { id: string; tenantId: string; key: string; // 'order', 'complaint', 'request' name: string; // 'Objednavka' icon: string; // klic do connectorIcons.ts statuses: string[]; // vlastni workflow, prazdne = vychozi ctverice fields: TicketTypeField[]; } ``` `Ticket` dostane dve pole: ```ts typeId: string | null; // null = ticket bez typu, jako dnes fields: Record; ``` Dve `id` u pole nejsou zbytecna. Je to totez rozdeleni, ktere uz v projektu je a je vysvetlene v [06-tickety.md](06-tickety.md): **podminky odkazuji na `id`**, aby prejmenovani nic nerozbilo, **sablony odkazuji na `key`**, aby si to clovek precetl. ### Typ nebo tag V zadani je otazka, jestli je "objednavka" tag. Odpoved je, ze jsou potreba **oba a jsou to jine veci**: | Vec | Kolik na ticket | K cemu | | ---- | --------------- | --------------------------------------------- | | Typ | prave jeden | vlastni pole a vlastni workflow stavu | | Tag | libovolne mnoho | volne oznaceni, filtry a widgety | **Akce se muze vazat na oboji**, ale nasledek se lisi: - **Na typ.** Za typem stoji pole, takze akce vi, ze existuje `orderNumber`, a muze ho dosadit do sablony. Kdyz akce potrebuje vlastni pole, musi byt na typu. - **Na tag.** Za tagem nestoji zadna pole, takze akce umi pracovat jen s vestavenymi polemi ticketu a s tim, na co se dopta formular. Zaroven tag muze kdokoliv pridat i odebrat, takze tlacitko se muze objevit u ticketu, ktery objednavka neni. U akce, ktera nekam neco odesle, je proto vhodne pridat `visibleWhen` nebo potvrzeni. Tagy jsou hlavne spravna vec pro widgety a filtry, tam je volnost prinos. `Ticket` proto dostane i `tags: string[]`. ### Definice akce `src/data/ticketActions.ts`. ```ts interface TicketAction { id: string; tenantId: string; /** Na co se vaze. Aspon jedno z obojiho, jinak by se nabizela vsude. */ ticketTypeIds: string[]; // prazdne = na typu nezalezi tags: string[]; // prazdne = na tagu nezalezi label: string; // 'Potvrdit objednavku' icon: string; style: 'primary' | 'default' | 'danger'; order: number; /** Kdy se tlacitko vubec ukaze. Vsechny podminky musi platit. */ visibleWhen: ActionCondition[]; /** Text potvrzovaciho dialogu, null = spusti se hned. */ confirm: string | null; /** Na co se doptat pred spustenim. Stejny typ jako inputs u akce konektoru. */ form: OperationField[]; /** Co akce dela. Tri moznosti, viz nize. */ body: ActionBody; /** Verze definice. Uprava zaklada novou, bezici behy si drzi svou. */ version: number; /** Pravo, ktere musi uzivatel mit. Vznika spolu s akci. Viz bod 5. */ permission: string; // 'action:tka_confirm_order' enabled: boolean; } type ActionBody = /** Jeden krok konektoru. Nejcastejsi pripad, nastavi se bez builderu. */ | { kind: 'operation'; connectorId: string; operationId: string; connectionId: string | null; inputs: Record } /** Vlastni strom. Tentyz model i tentyz builder jako u automatizaci. */ | { kind: 'tree'; steps: FlowStep[] } /** Vlastni skript. Podminky a limity jsou v bodu 9. */ | { kind: 'script'; scriptId: string; scriptVersion: number; inputs: Record }; ``` `visibleWhen` je nad `ticket.*` a `ticket.fields.*`, tedy nad uz existujicim modelem podminek. Typicky "stav je novy" nebo "objednavka jeste neni potvrzena". Tlacitko, ktere se ukaze vzdy a pak vyhodi chybu, je horsi nez tlacitko, ktere se neukaze. ### Tri druhy tela akce Akce neni jen jeden krok. `ActionBody` je proto union a **kazdy druh je jina uroven slozitosti pro tehoz cloveka**: | Druh | Kdy | UI | | ----------- | ---------------------------------------------- | --------------- | | `operation` | odesli tenhle doklad tam | formular | | `tree` | zkus to, a kdyz se to nepovede, dej to Karlovi | builder | | `script` | poskladej text v nasem formatu | editor skriptu | `tree` pouziva **tentyz model kroku, tentyz builder a tutez validaci** jako automatizace. Neni to druhy strom, je to ten samy strom na jinem miste. Kdyby to byl jiny model, mel by builder dve verze a jedna by zaostavala. `operation` je jen strom s jednim krokem, ale vyplati se ho mit zvlast: nejcasteji chce clovek presne tohle a nema chodit do builderu kvuli jednomu volani. Prevod z `operation` na `tree` musi jit jednim kliknutim, protoze potreba vetveni prijde az pri druhem selhani. `script` je pro pripad, kdy jde o prevod dat, ne o volani sluzby. Plati pro nej vsechna omezeni z bodu 9: nema sit, nema tajemstvi, vystupy deklaruje dopredu. ### Odkud akce vidi data ticketu Akce spustena z ticketu dostane vstupy ve tri skupinach. Pro builder je to totez jako `providedFields` u spoustece, takze se menit nemusi: | Zdroj | Priklad ID | Priklad v sablone | | ---------------------- | ----------------------- | --------------------- | | vestavena pole ticketu | `ticket.subject` | `{{subject}}` | | vlastni pole typu | `ticket.fld_order_no` | `{{orderNumber}}` | | doptavaci formular | `form.reason` | `{{reason}}` | Nabidka vstupu se pocita **za typ ticketu**, ne staticky z katalogu. To je jediny novy pripad: katalog dnes vraci pevny seznam. **Akce navazana na tag ma jen prvni a treti skupinu.** Za tagem nestoji zadna pole, takze sablona nema odkud vzit cislo objednavky. Je to podstatny prakticky rozdil: akce na tagu umi pracovat s predmetem, telem, zakaznikem a s tim, na co se dopta formular. Akce na typu umi navic vlastni pole. Kdyz akce potrebuje `orderNumber`, musi byt na typu. ### Jak to vypada na ticketu `GET /api/dashboard/tickets/:id` bude vracet navic: ```json "actions": [ { "id": "tka_confirm_order", "label": "Potvrdit objednavku", "icon": "check-circle", "style": "primary", "confirm": "Objednavka se posle do skladu. Pokracovat?", "form": [{ "id": "note", "label": "Poznamka", "kind": "text", "required": false }] } ] ``` Seznam **uz je filtrovany serverem** podle `visibleWhen`, typu ticketu a prav uzivatele. Klient jen kresli tlacitka. Je to totez pravidlo jako u `access` - klient si nic nedovozuje, jinak se to pocita na dvou mistech a jednou se rozejde. Spusteni: ``` POST /api/dashboard/tickets/:id/actions/:actionId body: { "form": { "note": "potvrzeno telefonicky" } } odpoved: 202 { "runId": "run_..." } ``` | Situace | Odpoved | | --------------------------------------- | ------- | | akce neexistuje nebo neni pro tento typ | 404 | | uzivatel na ni nema pravo | 403 | | `visibleWhen` neplati | 409 | | chybi povinne pole formulare | 400 | | automatizace je pozastavena | 409 | 409 u `visibleWhen` je zamer. Znamena "v tomhle stavu to nedava smysl", presne jako u ostatnich 409 v API. Uzivatel mel otevrenou starou stranku. ### Audit je soucast, ne pristavek Kazde stisknuti hned zapise zaznam do logu ticketu: ``` kind: 'action', status: 'info' label: 'Rucne spustil Karel Vomacka: Potvrdit objednavku' ``` Log ticketu uz je strom a lide do nej chodi. Zvlastni evidence "kdo co zmackl" by znamenala dve casove osy a hledani ve dvou mistech. Vysledek behu se pak zavesi pod tento zaznam jako jeho deti. ### Da se dodat pred runtimem Rucni akce nemusi cekat na frontu z posledni sekce. Kratky strom lze na zacatku vykonat **synchronne v requestu**, s limitem kroku a timeoutem. Podminka je napsat vykonavani jako `executeStep(step, context)`, kterou pozdeji zavola i worker. Ne jako kod uvnitr route handleru. Kdyz se to napise takhle, je rucni akce prvni uzivatel vykonavace a runtime pak nepridava novy kod, jen frontu nad nim. ## 3 - Rucni prehozeni a vestavene akce Cast uz existuje: `POST /tickets/:id/assign`, `POST /tickets/:id/status`, prava hlida `canAssignOthers`, v detailu ticketu jsou dva selecty. Navrh je **postavit vestavene akce do stejneho seznamu jako vlastni**. Ne jako zvlastni kus UI vedle nich. | ID | Co dela | Pravo | | ---------------------- | ------------------------------ | ------------------------- | | `builtin.assign.self` | vzit ticket na sebe | `ticket.assign.self` | | `builtin.assign.other` | prehodit na kolegu | `ticket.assign.others` | | `builtin.assign.group` | prehodit na skupinu | `ticket.assign.group` | | `builtin.status` | zmenit stav | `ticket.status.change` | | `builtin.priority` | zmenit prioritu | `ticket.priority.change` | | `builtin.type` | zmenit typ ticketu | `ticket.type.change` | | `builtin.reopen` | otevrit vyreseny | `ticket.reopen` | Dva prinosy: pravo se resi jednim mechanismem pro vestavene i vlastni akce, a admin muze vestavenou akci pro nekoho vypnout, aniz by se menil kod. ### Prehozeni na skupinu, ne jen na cloveka Bod 5 mluvi o ucetni a skladnikovi. To znamena, ze prehazovani na konkretni jmeno nestaci - clovek chce rict "tohle je pro ucetni" bez toho, aby resil, kdo z nich ma dovolenou. ```ts interface TicketGroup { id: string; tenantId: string; name: string; personIds: string[]; } ``` `Ticket` dostane `assigneeGroupId: string | null` vedle `assigneeId`. Fronta bez resitele se tim rozpadne na fronty skupin a prehled vytizeni dostane radek za skupinu. Je to vic prace nez zbytek bodu 3, takze druha vlna. ### Co jeste chybi k prehazovani - **Duvod prehozeni.** Nepovinne pole, zapise se jako komentar do logu. Bez nej se z logu neda poznat, proc ticket obesel tri lidi. - **Hromadne prehozeni ze seznamu.** Zaskrtavatka v seznamu ticketu, jedna akce nad vyberem. Server to musi resit jako N samostatnych zapisu s dilcim vysledkem, ne jako vse nebo nic - jeden ticket bez prava nesmi zabit zbytek. - **Notifikace.** Dnes se prirazeni objevi jen v SSE toastu, kdyz je clovek prihlaseny. Prehozeni na nekoho, kdo se prave nekouka, se ztrati. ## 5 - Kazdy spousti jen svuj seznam akci `TenantRole = 'admin' | 'agent'` na ucetni, skladnika a vedouciho nestaci. Pridavat dalsi hodnoty do unionu je slepa ulicka - kazdy klient chce jine. **Role a prava se stanou daty.** ```ts type PermissionKey = string; // 'ticket.assign.others', 'action:tka_confirm_order' interface Role { id: string; tenantId: string | null; // null = systemova role, dostupna vsem firmam key: string; // 'admin', 'agent', 'accountant' name: string; // 'Ucetni' permissions: PermissionKey[]; } interface Membership { tenantId: string; roleIds: string[]; // misto role: 'admin' | 'agent' } ``` `admin` a `agent` zustanou jako systemove role s prednastavenym seznamem prav. Nic se tim nerozbije a zadny existujici ucet se nemusi predelavat. ### Katalog prav Stejny princip jako katalog konektoru a widgetu: `src/data/permissions.ts` je zdroj pravdy o tom, jaka prava existuji, vcetne ceskych popisu a skupin. ``` GET /api/dashboard/permissions ``` Nastaveni role je pak zaskrtavatkova tabulka nad timto katalogem. Neznamy klic v roli se ignoruje a loguje, protoze pravo, ktere zmizelo z katalogu, nesmi shodit prihlaseni. Pravo k vlastni akci vznika **spolu s akci** jako `action:`. Admin pak nesestavuje prava, ale zaskrtava akce: "role Ucetni: Potvrdit objednavku, Vystavit fakturu". To je slovnik, ve kterem uvazuje. ### Kontrola na dvou mistech, ale ne dvakrat pocitana - **Server pri spusteni** je autorita. Vzdy, i kdyz klient tlacitko nezobrazil. - **Server pri vypisu** rozhodne, ktera tlacitka klient dostane. Klient nikdy prava nevyhodnocuje. Pri vypisu i pri spusteni se vola tataz funkce v `access.ts`. `Access` se rozsiri: ```ts interface Access { scopes: TicketScope[]; tenants: Tenant[]; defaultTenantId: string | null; personId: string | null; canAssignOthers: boolean; // zustava, pocita se z permissions permissions: PermissionKey[]; // efektivni prava ve zvolene firme nav: NavItem[]; // viz bod 6 } ``` **Pozor, tohle je nejvetsi zasah do stavajiciho kodu.** `accessFor(user)` uz nemuze stacit, prava jsou az uvnitr firmy: `accessFor(user, tenantId)`. Dotkne se to vsech rout, ktere `accessFor` volaji, a `requireRole` v `src/middleware/auth.ts` se nahradi `requirePermission(key)`. ## 6 - Admin: prepinani a nastavovani zalozek nad klienty Tri oddelene veci, ktere se snadno slijou do jedne. ### a) Prepinac firem Existuje. `platformAdmin` vidi vsechny firmy a pohled `all`. Beze zmeny. ### b) Ktere zalozky firma vubec ma ```ts interface TenantFeatures { tenantId: string; /** Ktere moduly firma ma. Klic odpovida zalozce v portalu. */ tabs: string[]; // ['overview','tickets','incidents','automations','connectors'] /** Ktere konektory smi pouzit. null = vsechny z katalogu. */ connectorIds: string[] | null; limits: { automations: number; widgets: number; customActions: number }; } ``` **Dve vrstvy s jinym vlastnikem, ktere se nesmi michat:** | Vrstva | Kdo nastavuje | Znamena | | --------------- | ------------------- | ------------------------------------ | | `TenantFeatures`| my, provozovatel | co ma firma zaplacene a zapnute | | `Role` | admin te firmy | kdo z jejich lidi to smi | Efektivni viditelnost je prunik. Vypnuty modul neexistuje ani pro admina te firmy - nema si ho jak zapnout, protoze ho nema. Kdyby to byla jedna vrstva, klientsky admin by si mohl zapnout to, co si nekoupil, nebo bychom my prenastavovali jejich interni prava. **Zpristupneni konektoru z bodu 9 je tataz vrstva.** `ConnectorGrant` a `TenantFeatures` odpovidaji na tutez otazku, jen jednou za modul a jednou za konektor, a nastavuje je tentyz clovek. Ma to proto byt **jedna obrazovka Nastaveni klienta**: zalozky, konektory, skripty, limity. Rozdelit to na tri obrazovky by znamenalo, ze pri onboardingu klienta se na jednu zapomene. Dusledek pro frontend: seznam zalozek v `web/src/components/dashboard/DashboardLayout.tsx` prestane byt konstanta a bude se brat z `access.nav`. Kompletni mapa cesta na komponentu zustane na klientovi, server posila jen klice a poradi. ### c) Prepnout se a videt to jako konkretni clovek To uz neni prepinac firem, ale impersonace, a je to citliva vec. Navrh: ``` POST /api/admin/impersonate { "userId": "usr_..." } -> kratkodoby token POST /api/admin/impersonate/stop ``` Pravidla: - smi jen `platformAdmin`, a **nikdy na jineho** `platformAdmin`, - token plati nejvyse 30 minut a neda se obnovit, v payloadu nese `act` s ID skutecneho cloveka, - **vychozi rezim je jen cteni.** Zapis se musi vyslovne zapnout pri startu impersonace a kazdy zapis pak nese v auditu `actedBy`, - v portalu je po celou dobu vyrazny pruh s tim, za koho se uzivatel diva, a tlacitkem zpet. Bez nej clovek zapomene a smaze neco cizim jmenem, - start, konec i kazda akce jde do auditu. ### Audit log Bez nej nema impersonace smysl a `07-firmy-a-prava.md` ho uz vede jako chybejici. ```ts interface AuditEntry { id: string; at: string; tenantId: string | null; userId: string; actedBy: string | null; // vyplnene pri impersonaci action: string; // 'ticket.assign', 'role.update', 'impersonate.start' target: string | null; detail: Record; result: 'ok' | 'denied'; } ``` Zapisuji se i **odepreni**. Dnes se jen loguji do konzole, takze opakovane pokusy o cizi firmu nikde nezustanou. ## 4 - Vlastni widgety Dnes je `widgets.ts` pevny katalog, kde `kind` rika, kterou komponentu vykreslit. Pro vlastni widgety se to rozdeli na **jak se to kresli** a **odkud jsou data**. ```ts type WidgetRender = 'stat' | 'chart' | 'list' | 'table' | 'gauge'; type WidgetSource = | { kind: 'ticketCount'; filter: TicketFilter; groupBy?: WidgetGroupBy } | { kind: 'ticketList'; filter: TicketFilter; limit: number } | { kind: 'ticketSeries'; filter: TicketFilter; bucket: 'day' | 'week' } | { kind: 'runCount'; filter: RunFilter; groupBy?: WidgetGroupBy } | { kind: 'workload'; groupIds?: string[] } | { kind: 'connectorMetric'; connectionId: string; metricId: string; period: Period }; type WidgetGroupBy = 'assignee' | 'group' | 'status' | 'type' | 'tag' | 'channel'; interface CustomWidget { id: string; tenantId: string; ownerId: string | null; // null = firemni widget, jinak osobni name: string; render: WidgetRender; source: WidgetSource; target?: number; // porovnavaci hodnota u gauge sizes: WidgetSize[]; defaultSize: WidgetSize; } ``` ### Uzivatel nepise dotazy `TicketFilter` je **tentyz filtr, ktery uz umi `GET /api/dashboard/tickets`**: stav, typ, priorita, kanal, resitel, skupina, obdobi. Validuje ho totez schema. Tim je bod "widgety skrz stavy ticketu" hotovy bez jedineho noveho dotazu do dat a bez jakekoliv moznosti napsat neco, co server polozi. Kdo si vymysli filtr, ktery seznam ticketu neumi, dostane 400, a je to zaroven signal, ze ten filtr patri nejdriv do seznamu. ### Widgety skrz konektory Katalog konektoru dostane u operaci treti druh vedle `triggers` a `actions`: ```ts interface ConnectorMetric { id: string; name: string; /** number = jedno cislo, series = casova rada, list = radky */ shape: 'number' | 'series' | 'list'; unit?: string; periods: Period[]; } ``` Widget je pak "konektor Fakturoid, metrika Neuhrazene faktury, za 30 dni". Katalog zustava zdrojem pravdy presne jako u triggeru: pridani metriky je zaznam v katalogu, ne novy kod na webu. **Widget odkazuje na napojeni, ne na konektor.** Duvod je v bodu 9: metrika se tahne z konkretniho uctu s konkretnimi pristupy. Firma bez napojeni na iDoklad si takovy widget nemuze postavit, i kdyz iDoklad v katalogu vidi. A kdyz se napojeni zrusi, widget musi na svem miste rict, ze napojeni chybi - ne zhasnout prehled a ne ukazovat posledni znamou hodnotu jako aktualni. ### Seskupovani `groupBy` je to, co dela z dlazdice pouzitelny prehled. "Tickety ve stavu selhalo vuci lidem, u kterych jsou" je `ticketCount` s filtrem na stav a `groupBy: 'assignee'`. Bez seskupovani by to byl jeden widget na cloveka a po prichodu noveho kolegy by chybel. Vysledek seskupovani je seznam par nazev a cislo. Vykresli se jako pruhy, tabulka nebo kruh podle `render` - jsou to tataz data. ### Poznamka k "selhalo" Ticket dnes ma stavy novy, otevreny, ceka a vyresen. Zadne selhalo. Ta otazka se musi rozhodnout, protoze jsou to dve rozdilne veci: - **selhal beh** automatizace nebo akce. Pak je to `runCount`, ne `ticketCount`, a widget se pta na historii behu z bodu o runtimu. - **ticket je v koncovem stavu, ktery znamena nezdar** (objednavku neslo odeslat). Pak je to vlastni stav v `TicketType.statuses`, tedy vlastni workflow za typ. Nejcasteji chce clovek prvni. Druhe je uzitecne az tam, kde typ ticketu ma opravdu jiny zivotni cyklus. ### Dochazka neni widget Dochazka v aplikaci neexistuje. Je to samostatna domena, ne druh dlazdice. Spravna cesta je **konektor dochazky** s metrikami (kdo je dnes v praci, hodiny za mesic, chybejici prichody). Widget pak vznikne sam, protoze `connectorMetric` uz bude umet. Delat pro dochazku zvlastni typ widgetu by znamenalo, ze pro kazdou dalsi oblast pribude dalsi. ### Nacitani dat Dnesni pravidlo je "data si nacita prehled, ne widgety" a je spravne. S vlastnimi widgety uz ale nejde predpovedet, kolik dotazu je potreba, takze: ``` POST /api/dashboard/widgets/data body: { "scope": "tenant", "tenantId": "tnt_automia", "instanceIds": ["w1","w2","w3"] } odpoved: { "w1": {...}, "w2": {...}, "w3": { "error": "..." } } ``` Jeden request na cely dashboard. Server si vysledky cachuje na par sekund podle podpisu zdroje, takze deset dlazdic nad stejnym filtrem je jeden dotaz. Widget, ktery selze, vraci chybu **na sve pozici** - jeden rozbity konektor nesmi zhasnout cely prehled. Obnovovani zustava na `refetchOn` a SSE. ### Sdilene rozlozeni Rozlozeni je dnes za dvojici uzivatel a firma. Pribude treti uroven: 1. rozlozeni uzivatele, kdyz si ho upravil, 2. jinak rozlozeni pro jeho roli ve firme, kdyz ho admin nastavil, 3. jinak vychozi z katalogu. Novy clovek v roli Skladnik tak hned uvidi dashboard skladnika, ne obecny. ## 7 - Napojeni na Postgres Ano, a je to prvni vec, kterou udelat. Vsechno ostatni na ni stoji: role, akce, widgety, audit i historie behu jsou data, ktera nesmi zmizet restartem. Aplikace je na to pripravena. `src/data/` je oddelene od rout a `03-architektura-a-mapa-kodu.md` s tim pocita. Cilem je **zachovat signatury funkci ulozist** a prepsat jim vnitrek. Routy se menit nemaji. ### Volby | Vec | Navrh | Proc | | ------------- | ------------------------------------ | ------------------------------------------- | | Databaze | PostgreSQL 16 | JSONB, `SKIP LOCKED`, `LISTEN/NOTIFY` | | Driver | `pg` | bez nadstavby, pool | | Dotazy | Drizzle (nebo Kysely) | typovane SQL, ne skryty ORM | | Migrace | `drizzle-kit`, soubory v repu | deterministicke, dohledatelne v gitu | | Pripojeni | `DATABASE_URL` jako AppFactory secret| nikdy v kodu, nikdy v logu | Prisny ORM se nedoporucuje. Cely projekt je psany tak, ze server je autorita a filtr na firmu je povinny argument - to se hlida lip nad viditelnym SQL. ### Schema, na cem zalezi - **`tenant_id NOT NULL` na kazde business tabulce**, indexy zacinaji `tenant_id`. - Povinny argument `tenantIds` v ulozistich **zustava**. Je to prvni obrana a funguje uz pri kompilaci. Row Level Security se da pridat pozdeji jako druha vrstva, ne jako nahrada. - JSONB na to, co se cte a zapisuje cele: strom automatizace, vlastni pole ticketu, zdroj widgetu, vstup a vystup kroku. Strom akci nerozkladat do tabulek, nikdo se nad nim nedotazuje po castech. - Cizi klice s `ON DELETE RESTRICT` u vsecho, co nese historii. Smazany resitel dnes ticket neshodi, to chovani musi zustat. ### Poradi prevodu 1. `tenants`, `users`, `roles`, `people` - identita, nejmensi objem, nejvic zavislosti. 2. `tickets`, `ticket_trace`, `ticket_types`, `ticket_tags`, `ticket_actions`. 3. `connector_defs`, `connector_grants`, `connections`, `scripts` - bod 9. 4. `automations`, `runs`, `run_steps`, `jobs` - stromy a runtime. 5. `message_templates`, `dashboard_layouts`, `custom_widgets`, `audit`. Sifrovani pristupovych udaju z bodu 9 patri do treti skupiny a **nesmi se odlozit na potom**. Napojeni ulozene v plaintextu s tim, ze se to dosifruje pozdeji, je uz proteklo do zaloh. ### Udalosti pres LISTEN/NOTIFY, ne Redis `src/events/bus.ts` je dnes `EventEmitter` v pameti jedne instance a dokumentace u toho pocita s Redisem. S Postgresem to neni potreba: `publish()` zapise udalost do tabulky a zavola `pg_notify`, `subscribe()` si drzi jedno pripojeni s `LISTEN`. Signatury zustanou stejne, SSE stream se nemeni. Dve veci k tomu: - **`pg_notify` ma limit 8000 bajtu.** Posilat jen typ a ID, ne cela data. Nevadi to, `useApiQuery` uz na udalost stejne dela nove nacteni. - Tabulka udalosti navic vyresi "poslednich par udalosti po pripojeni", ktere dnes zmizi restartem. ### Health endpoint `/health` musi podle `AGENTS.md` vracet 200, kdyz aplikace zvladne obsluhovat provoz. **Nezavazovat ho na databazi.** Kratky vypadek DB by AppFactory vedl k restartovani containeru, coz nic nespravi. Ping na databazi patri na `/health/ready` s vysledkem cachovanym par sekund. ## 8 - Cekani v automatizaci a sablony zprav ### Krok "pockej" Novy druh kroku vedle akce a podminky: ```ts | { kind: 'wait'; mode: 'duration'; amount: number; unit: 'minutes' | 'hours' | 'days' } | { kind: 'wait'; mode: 'business'; amount: number; unit: 'hours' | 'days' } // v pracovni dobe firmy | { kind: 'wait'; mode: 'until'; at: string } // sablona, napr. {{dueDate}} | { kind: 'wait'; mode: 'event'; eventType: string; matchOn: string; timeout: WaitDuration } // pockej na odpoved ``` **Implementacne je to skoro zdarma**, a je to hlavni odmena za frontu v databazi z posledni sekce. Tabulka `job` uz ma `run_after`, takze "pockej 3 dny" je jeden `UPDATE` s posunutym casem. Zadny casovac v pameti, zadny `setTimeout`, restart containeru se behu nedotkne. S casovacem v pameti by tenhle bod nesel udelat vubec. Ctyri veci, ktere se u cekani snadno zapomenou: - **Pracovni doba.** "Pockej 2 dny a posli upominku" u SLA znamena dva **pracovni** dny. Potrebuje to kalendar za firmu: pracovni doba, dny v tydnu, statni svatky, casova zona. Dodelat to potom znamena predelat vsechny existujici cekaci kroky, proto to ma byt v modelu od zacatku, i kdyby UI zpocatku nabizelo jen `duration`. - **Zruseni.** "Za 3 dny posli upominku" nesmi odejit, kdyz je ticket mezitim vyreseny. Nejlevnejsi a nejsrozumitelnejsi je **podminka pri probuzeni**: krok cekani ma `cancelWhen`, ktere se vyhodnoti az v okamziku probuzeni. K tomu tlacitko "zrusit beh" na ticketu. - **Verze definice.** Beh, ktery ceka tyden, musi dobehnout podle stromu, ktery platil pri jeho spusteni. Proto se verze automatizace nebo akce pinuje na zacatku behu (`source_version` v tabulce `run`). Bez toho by uprava stromu zmenila chovani nekolika tisic cekajicich behu. - **Prehled cekajicich behu.** Kdyz ceka 30 tisic behu, musi byt videt, na cem ceka a kdy se probudi, a musi jit hromadne zrusit. Jinak to je neviditelna bomba s casovym spinacem. Casy se ukladaji v UTC. Probuzeni se pocita jako **absolutni okamzik** dopredu, v casove zone firmy, aby prechod na letni cas neposunul vsechno o hodinu. ### Sablony zprav Dnes se text pise do pole akce ve strome. To staci na jeden radek, ne na e-mail. Sablona je proto samostatna vec: ```ts interface MessageTemplate { id: string; tenantId: string; key: string; // 'order-confirmed' name: string; // 'Potvrzeni objednavky' channel: 'email' | 'whatsapp' | 'sms'; subject: string; // sablona bodyText: string; // sablona, vzdy povinna bodyHtml: string | null; // jen u e-mailu, nepovinna /** Ktere parametry sablona potrebuje. Klic k validaci, viz nize. */ expects: string[]; locale: string; } ``` **Zadny novy sablonovaci jazyk.** Tentyz `{{parametr}}` a tentyz kod v `src/data/templates.ts`. Dva sablonovaci systemy v jedne aplikaci znamenaji, ze v jednom pujde `{{a.b}}` a v druhem ne, a nikdo nebude vedet, ve kterem prave je. **`expects` je to podstatne.** Sablona sama nevi, odkud se pouzije, takze pri ulozeni nema proti cemu overit `{{orderNumber}}`. Deklaruje proto, co potrebuje, a **krok, ktery ji pouzije, si overi, ze to jeho misto ve strome nabizi**. Tim se z prekvapeni pri behu stane `issue` v builderu, presne v tom rezimu, ktery uz projekt ma: nedodelek se ulozi, jen automatizaci nepusti. Dal k tomu patri: - **Rozvrzeni za firmu.** Hlavicka, logo, paticka a podpis jednou, ne v kazde sablone. Sablona nese jen telo. - **Nahled s ukazkovymi daty.** `POST /api/dashboard/templates/:id/preview` s hodnotami parametru. Bez nahledu se sablony ladi odesilanim skutecnych e-mailu zakaznikum. - **Cisteni HTML.** Telo od uzivatele projde sanitizaci, styly inline. Jinak je to XSS na kazdem, kdo si e-mail otevre ve webmailu. ### Odesilani e-mailu vubec neexistuje `01-prehled-a-stav.md` vede odesilani e-mailu jako chybejici, dnes se poptavka jen loguje. Bod 8 tedy potrebuje i **transport**: napojeni na SMTP nebo poskytovatele za firmu, jako kterekoliv jine napojeni z bodu 9. Dve veci, ktere se resi az bolestive pozde: - **Odpoved zakaznika se ma vratit do ticketu**, ne zalozit novy. Znamena to adresu s tokenem (`ticket+@...`) a parovani pri prijmu. Kdyz se to nezavede hned, vznikne z kazde e-mailove konverzace pet ticketu. - **Doruditelnost.** SPF, DKIM a DMARC na domene klienta, jinak upominky konci ve spamu a nikdo se to nedozvi. Patri to do onboardingu klienta, ne do kodu. ## 9 - Jak jsou postavene konektory a skripty Tohle je nejdulezitejsi cast celeho navrhu, protoze rozhoduje o bezpecnostnim modelu. Dnes je katalog v `src/data/connectors.ts` spolecny pro vsechny firmy a `07-firmy-a-prava.md` vede "Tenant u konektoru" jako chybejici. ### Tri vrstvy, ktere se nesmi slit do jedne Priklad ze zadani (vsichni mohou iDoklad, jen firma C vidi Polstryn SAP, firma B iDoklad vidi ale nema napojeni) nejde zapsat mene nez tremi vrstvami: | Vrstva | Co to je | Kdo to vlastni | | --------------- | ------------------------------------ | -------------------- | | **Definice** | ze iDoklad existuje a co umi | my, nebo firma | | **Zpristupneni**| kdo ho vubec smi videt | my | | **Napojeni** | ucet firmy s jejimi pristupy | firma | ```ts interface ConnectorDefinition { id: string; key: string; // 'idoklad' name: string; category: ConnectorCategory; icon: string; /** platform = udelali jsme my, tenant = udelala si firma sama. */ ownerScope: 'platform' | 'tenant'; ownerTenantId: string | null; /** public = vidi ho vsichni. restricted = jen komu je zpristupneny. */ visibility: 'public' | 'restricted'; status: 'available' | 'planned' | 'deprecated'; /** Jak se autorizuje: co se vyplnuje pri zakladani napojeni. */ auth: ConnectorAuth; triggers: ConnectorOperation[]; actions: ConnectorOperation[]; metrics: ConnectorMetric[]; version: number; } /** Zpristupneni definice konkretni firme. Jen u visibility: 'restricted'. */ interface ConnectorGrant { connectorDefId: string; tenantId: string; grantedBy: string; grantedAt: string; } interface ConnectorConnection { id: string; tenantId: string; connectorDefId: string; name: string; // 'iDoklad Celo' - firma muze mit dve /** Sifrovane, nikdy se nevraci z API. Viz nize. */ secrets: EncryptedBlob; /** Necitliva cast nastaveni: URL instance, cislo eshopu, vychozi rada. */ config: Record; status: 'active' | 'error' | 'expired'; isDefault: boolean; lastCheckAt: string | null; lastError: string | null; } ``` ### Stav konektoru se prestane cist a zacne pocitat Dnes je `ConnectorStatus = 'connected' | 'available' | 'planned'` pevne pole v katalogu. Ty tri hodnoty jsou ale presne to, co zadani popisuje, takze staci je **pocitat za firmu**: | Stav | Kdy | | ----------- | ------------------------------------------------------- | | `connected` | firma ma aktivni napojeni | | `available` | vidi definici, napojeni nema, muze si ho udelat | | `planned` | definice je na roadmape | | neviditelny | `restricted` bez zpristupneni - v odpovedi vubec neni | `GET /api/dashboard/connectors` tim prestava byt spolecny a zacina byt za firmu. Neviditelna definice se **nevraci se stavem, ale nevraci se vubec**. Firma A nesmi z odpovedi poznat, ze Polstryn SAP existuje. ### Krok ve strome odkazuje na napojeni `FlowStep` dnes nese `connectorId` a `operationId`. Pribude `connectionId`: ```ts { kind: 'action'; connectorId: string; operationId: string; connectionId: string | null; // null = vychozi napojeni firmy pro tento konektor inputs?: Record } ``` Proc oboji a proc nullable: - **Explicitni `connectionId`** je potreba, kdyz firma ma dva ucty teze sluzby. - **`null` jako vychozi** dela strom prenositelnym. Vzorovy strom se da nabidnout vsem firmam a kazde se dosadi jeji vlastni napojeni. Validace pri ulozeni: napojeni musi patrit **te same firme** a **te same definici**. Cizi `connectionId` se chova jako neexistujici, tedy 404, ne 403 - stejne pravidlo jako u ticketu v `07-firmy-a-prava.md`. Chybejici vychozi napojeni je nedodelek, ne chyba: strom se ulozi, automatizace nepujde zapnout. ### Pristupove udaje Tohle je nejcitlivejsi misto cele aplikace. - **Sifrovane v Postgresu**, AES-256-GCM, klic z AppFactory secretu, nahodne IV za zaznam a `keyVersion` u kazdeho radku, aby sla vymena klice. - **Pole jsou jen pro zapis.** API umi `clientSecret` nastavit a nikdy ho nevrati. Odpoved nese `{ "clientSecret": { "isSet": true } }`, ne hodnotu. To je presne to, co `AGENTS.md` zakazuje: secrets se nevraci z beznych endpointu. - **Redakce v logu ticketu.** Log ukazuje, co sluzba vratila, a to je jinak jista cesta, jak se token dostane do trace, kdyz ho cizi API vrati v odpovedi nebo v chybove zprave. Pred zapisem musi projit nahrada znamych tajnych hodnot za hvezdicky. **Neni to volitelne dolazeni**, je to soucast zapisu do logu. - **OAuth spravuje runtime.** Definice deklaruje token endpoint, runtime drzi access token s expiraci a obnovuje ho. Skript ani sablona se k tokenu nedostanou. - **Test napojeni** jako samostatny endpoint. Firma po zadani udaju zmackne Overit a hned vi, jestli to funguje, misto aby to zjistila z padle automatizace v noci. ### Definice v DB, ale nase definice patri do repa Zadani rika dat konektory do DB. Ano, s jednou vyhradou, ktera se vyplati: - **Definice, ktere delame my** (iDoklad, Shoptet, WhatsApp) zijou **v repu** a do DB se nasazuji migraci nebo seedem. Repo je zdroj pravdy. Jinak zmizi code review, historie v gitu i moznost vratit zmenu, a vyvoj se rozejde s produkci. Rucne upraveny radek v produkci nikdo za tri mesice nedohleda. - **Definice, ktere si udela firma**, zijou jen v DB. Tam repo neni od ceho, vlastnikem je zakaznik. Runtime v obou pripadech cte z DB, takze se kod nemusi rozdvojovat. ### Jak se konektor stavi Definice ma u kazde operace **implementaci**, a jsou tri druhy: | Druh | Kde je kod | Pro co | | --------- | ----------------- | ----------------------------------------- | | `builtin` | v repu, TypeScript| nase konektory, kde potrebujeme plnou moc | | `http` | zadny kod | vetsina REST API, i to, co si udela firma | | `script` | sandbox | prevod dat a divne protokoly | **`http` ma byt vychozi**, i pro nase konektory. Operace je pak zaznam: ```ts interface HttpImplementation { method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; path: string; // sablona nad inputs a config headers: Record; // sablony, tajemstvi jen odkazem body: string | null; // sablona /** Mapovani odpovedi na outputFields: cesta v JSONu na nazev vystupu. */ outputMap: Record; // { customerId: 'data.id' } pagination?: { kind: 'page' | 'cursor'; ... }; /** Ktere HTTP kody jsou opakovatelne. Zbytek je koncova chyba. */ retryOn: number[]; } ``` V portalu je to formular: adresa, metoda, hlavicky, telo, mapovani vystupu a tlacitko Vyzkouset s ukazkovymi daty. Tim je odpovezena i puvodni otazka "vytvorit produkt skrz custom skripty" - vetsinou to neni skript, ale jedno POST volani a mapovani odpovedi. A co je dulezitejsi, plati pro nej audit, retry i rate limit jako pro kazdy jiny krok. U `http` se musi ohlidat SSRF: povolene domeny za firmu, zakaz privatnich rozsahu IP a localhostu, timeout, limit velikosti odpovedi, zadne nasledovani presmerovani na jinou domenu. ### Skripty Zbyde potreba prepocitat, spojit nebo preformatovat data mezi dvema kroky. ```ts interface ScriptDef { id: string; name: string; ownerScope: 'platform' | 'tenant'; ownerTenantId: string | null; visibility: 'public' | 'restricted'; // stejny grant jako u konektoru code: string; version: number; status: 'draft' | 'published' | 'disabled'; inputs: OperationField[]; outputFields: ProvidedField[]; // deklarovane dopredu } ``` Pravidla, na kterych to stoji: - **Skript nema sit ani tajemstvi.** Vstup JSON, vystup JSON, nic jineho. Cizi sluzby vola runtime pres konektory. Jinak zmizi audit, retry i rate limit a padly skript necha rozdelanou praci, kterou nikdo nedohleda. - **`node:vm` neni bezpecnostni hranice** a nesmi se pouzit. Bud `isolated-vm`, nebo QuickJS ve WASM. - Limity: CPU radove desitky ms, pamet jednotky MB, velikost vystupu, zadne `require`, zadne `fetch`, zadne casovace. - **Vystupy se deklaruji dopredu**, aby na ne sla postavit podminka jako na kazdy jiny `outputFields`. Skript vracejici cokoliv by se v builderu nedal pouzit. - **Publikovana verze je nemenna**, uprava zaklada novou. Totez jako u definice akce a ze stejneho duvodu. Kdo potrebuje libovolne IO, nedostane skript, ale **vlastni sluzbu v AppFactory** - presne k tomu AppFactory je - a v katalogu definici konektoru, ktera ji vola. Tim se z vyjimky stava normalni konektor. ### Ctyri ruzne otazky, ktere se pletou do jedne U konektoru i skriptu se musi rozlisit, a kazda ma jineho vlastnika: | Otazka | Mechanismus | Nastavuje | | --------------------------------- | -------------------------- | --------------- | | Kdo to **vidi** | `visibility` plus grant | my | | Kdo si smi udelat **napojeni** | pravo `connector.manage` | admin firmy | | Kdo to smi **pouzit ve strome** | pravo `automation.edit` | admin firmy | | Kdo smi **spustit** rucni akci | pravo `action:` | admin firmy | | Kdo smi **upravit skript** | pravo `script.edit` | my | Zvlast posledni radek: uprava skriptu zmeni chovani vseho, co ho pouziva. To neni pravo, ktere se dava vedle prava zakladat tickety. ## Cele flow z prikladu Kontrola navrhu proti zadani. Karel Novak, firmy Celo a Delo. ### Zaznamy, ktere vzniknou ``` users usr_novak Karel Novak memberships: [ (tnt_celo, admin), (tnt_delo, admin) ] connector_def cd_idoklad iDoklad platform public cd_easyweb EasyWeb platform public cd_shoptet Shoptet platform public cd_sap_pol Polstryn SAP platform restricted connector_grant (cd_sap_pol, tnt_firmac) connection cn_1 tnt_celo cd_idoklad 'iDoklad Celo' secrets: clientId+secret cn_2 tnt_celo cd_easyweb 'EasyWeb Celo' cn_3 tnt_delo cd_shoptet 'Shoptet Delo' ticket_type tt_order tnt_celo key 'order' 'Objednavka' fields: orderNumber, customerName, total ticket_action tka_send tnt_celo [tt_order] 'Odeslat do iDokladu' body: operation cd_idoklad/create-invoice cn_1 inputs: { number: '{{orderNumber}}', ... } tka_txt tnt_celo [tt_order] 'Vytvorit textak' body: tree [ skript poskladej text, Soubory/create ] automation au_order tnt_celo trigger: EasyWeb objednavka prijata (cn_2) steps: [ Zalozit ticket typu order, iDoklad/create-invoice pres cn_1 ] ``` ### Co Karel vidi ve firme Celo Katalog konektoru: iDoklad `connected`, EasyWeb `connected`, Shoptet `available` (vidi, ze si ho muze napojit, ale ve strome ho pouzit nejde). Polstryn SAP v odpovedi neni vubec. Ticket typu Objednavka: dve tlacitka, Odeslat do iDokladu a Vytvorit textak. Obe smi zmacknout, protoze ma roli s pravy `action:tka_send` a `action:tka_txt`. Automatizace `au_order` dela to same odesilani sama, ma ten krok ve svem strome. Odesilani do iDokladu je tedy nastavene na dvou mistech: v akci `tka_send` a ve strome `au_order`. **To je ta vedoma cena z uvodni sekce.** Obe mista pritom volaji tutez operaci katalogu pres totez napojeni `cn_1`, takze co se duplikuje, je mapovani poli, ne logika. ### Co Karel vidi ve firme Delo Prepne firmu. Katalog: Shoptet `connected`, iDoklad a EasyWeb `available` (neni napojeni, jde si ho udelat). Polstryn SAP opet vubec. Typ ticketu Objednavka **neexistuje**, patri firme Celo. Tlacitka `tka_send` a `tka_txt` taky ne, a to ani kdyby si typ zalozil - definice akce nese `tenantId`. To je ten vysledek, o ktery v zadani jde: **tentyz clovek, tytez prihlaseni, uplne jina nabidka.** Drzi to jedina vec, a to `tenantId` na definici akce, typu ticketu, automatizace i napojeni. Kdyby kterakoliv z nich `tenantId` nemela, prosakne firma Celo do Dela. ### Textovy soubor: pozor na jednu past "Vlastni akce mi ulozi textak s nejakym formatem" nejde udelat skriptem, protoze skript nema IO. A hlavne: **container ma docasny disk**. Soubor zapsany na disk zmizi pri kazdem nasazeni. Spravne je vestaveny konektor Soubory s akcemi `create` (obsah je sablona, vystup je odkaz), `attach-to-ticket` a `send`. Ulozeni jde do objektoveho uloziste, ne na disk containeru, a soubor se objevi jako priloha ticketu nebo ke stazeni. Skript, kdyz je vubec potreba, jen **poskladej text** a vrati ho jako hodnotu. Znamena to jednu infrastrukturni vec navic: S3 kompatibilni uloziste, treba MinIO. Je to jediny bod celeho navrhu, ktery si rika o dalsi sluzbu vedle Postgresu. ### Widgety z prikladu | Widget | Zdroj | | ---------------------------------------- | -------------------------------------------------- | | Pocet objednavek od-do | `connectorMetric` nad `cn_1`, metrika a obdobi | | Pocet novych klientu od-do | `connectorMetric` nad `cn_1` | | Pocet ticketu typu Objednavka | `ticketCount`, filtr `typeIds: [tt_order]` | | Pocet padlych behu | `runCount`, filtr `status: failed` | | Padle tickety vuci lidem | `ticketCount` plus `groupBy: 'assignee'` | Prvni dva se ve firme Delo postavit nedaji, protoze `cn_1` do ni nepatri. Az bude mit Delo svuj iDoklad, postavi si je nad svym napojenim a cisla budou jeji. ## Runtime a kapacita Vyhodnocovani kroku, prijem udalosti a co to znamena pri 150 klientech ma vlastni soubor: [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md). Odsud je dulezite jen tolik, ze prubeh behu drzi radek v databazi, ne pamet procesu, a ze diky tomu je cekaci krok z bodu 8 skoro zdarma. ## Poradi prace | Vlna | Co | Zavisi na | | ---- | ----------------------------------------------------- | --------- | | 0 | Postgres, prevod ulozist, LISTEN/NOTIFY (bod 7) | - | | 1 | Konektory do DB: definice, zpristupneni, napojeni, sifrovani udaju (bod 9) | 0 | | 2 | Definice akci: telo jako operace, strom nebo skript, verzovani | 0, 1 | | 3 | Typy ticketu, tagy, vlastni pole, rucni akce, vestavene akce (1, 2, 3) | 0, 2 | | 4 | Role a prava jako data, zalozky ze serveru, Nastaveni klienta, audit, impersonace (5, 6) | 0, 1 | | 5 | Runtime: fronta, retry, idempotence, fairness, krok `wait` (bod 8) | 0, 2 | | 6 | Sablony zprav a odesilani e-mailu (bod 8.1) | 0, 5 | | 7 | Vlastni widgety, seskupovani, sdilene rozlozeni (bod 4) | 0, 1 | | 8 | Skripty v sandboxu | 1, 5 | Zmena proti prvni verzi: **konektory se posunuly na zacatek**. Bez rozdeleni na definici, zpristupneni a napojeni nema smysl delat typy ticketu ani widgety, protoze oboji uz na napojeni odkazuje. Prvni verze mela konektory az v posledni vlne a byla to chyba. Vlna 7 je nezavisla na 2 az 6 a da se zaradit kdykoliv po 1, kdyz je potreba neco ukazat. Vlna 3 nemusi cekat na 5, protoze rucni akce se zpocatku vykona synchronne - viz konec sekce 1 a 2. Vlna 8 je posledni zamerne. Az bude hotovy `http` druh implementace z bodu 9, casto se ukaze, ze skripty nikdo nepotrebuje. ## Co se tim rozbije Seznam mist, ktera navrh meni a je potreba je hlidat. | Zmena | Dotkne se | | -------------------------------------------- | ------------------------------------------------------ | | `accessFor(user)` -> `accessFor(user, tenantId)` | vsechny routy dashboardu, prava jsou az uvnitr firmy | | `Membership.role` -> `roleIds` | `types.ts`, `users.ts`, `access.ts`, `middleware/auth.ts` | | `requireRole` -> `requirePermission` | `src/middleware/auth.ts` a vsechna jeho pouziti | | `Ticket` dostane `typeId` a `fields` | `ticketStore.ts`, `openapi.ts`, `web/src/types/dashboard.ts`, seznam, detail, simulace | | Katalog konektoru prestane byt spolecny | `connectors.ts`, `GET /connectors`, `Connectors.tsx` - vraci se za firmu | | `ConnectorStatus` se prestane cist z katalogu | pocita se z napojeni, dnes je to pevne pole | | `FlowStep` dostane `connectionId` | `automationStore.ts`, `flow.ts`, validace stromu, builder | | Zalozky ze serveru | `web/src/components/dashboard/DashboardLayout.tsx`, dnes konstanta | | `WidgetKind` -> `render` plus `source` | `widgets.ts`, `WidgetCard.tsx`, ulozena rozlozeni potrebuji prevod ID | | Novy druh kroku `wait` a `call` | `flow.ts`, `flowScope.ts`, `FlowCanvas.tsx`, validace | | `visibleWhen` potrebuje AND vice podminek | model podminek dnes umi jedno porovnani, viz `conditions.ts` | | Log ticketu potrebuje redakci tajemstvi | `ticketStore.ts`, zapis `response` do trace | | Data z pameti do Postgresu | cele `src/data/`, routy zustavaji | Dve veci k modelu podminek. `visibleWhen` u akce potrebuje spojit vic porovnani, zatim to jde jen vnorenim ve strome. Bud model podminek rozsirit o seznam s AND na obou stranach (`src/data/conditions.ts` i `web/src/lib/flow.ts`, vzdy obe), nebo AND drzet jen u akci a nemichat to do stromu. Druha varianta je levnejsi, prvni upravnejsi. A pri kazdem novem endpointu soucasne `src/openapi.ts` a tuhle dokumentaci. Swagger, ktery neodpovida chovani, je horsi nez zadny.