Konektory dostaly vykonnou cast. Jeden skript je jeden soubor, ktery nese manifest (vstupni a vystupni parametry) i kod. Diky manifestu s nim umi pracovat strom automatizace, aniz by o kodu cokoliv vedel. Soubory jsou zamerne obycejny JavaScript, ne TypeScript. TypeScript by se musel prelozit a to je presne to otaceni, ktere tady nema byt. Registr sleduje cas zmeny souboru, takze uprava v portalu, rucni uprava souboru i novy soubor ve slozce funguji stejne a bez restartu. Pridano: - scripts/ se skripty konektoru, nazev souboru je zaroven ID operace - kontrola vstupu i vystupu proti manifestu, jedna funkce pro obe strany. Chybejici povinny vystup je chyba skriptu, ne uzivatele - jinak by strom veril parametru, ktery nikdy nedosel - ctx predavany skriptu: http nad adresou napojeni, util, log, config, idempotencyKey, fail a retry. Skript nedostane pristupove udaje - rozliseni opakovatelne a koncove chyby. Runner nikdy nevyhodi vyjimku, vzdy vraci vysledek vcetne retryable - redakce tajnych hodnot pred zapisem do logu. Cizi API rado vraci prijaty token v chybove zprave a log ticketu vidi klient - napojeni z environment variables vcetne iDokladu - sest ukazkovych skriptu pro iDoklad proti skutecnemu API sluzby services.csbot.cz/apps/idoklad, kazdy na jiny vzor - stranka /dashboard/skripty: seznam, manifest, editor, zkusebni spusteni. Formular testu se sklada z manifestu, nepise se pro kazdy skript - endpointy /api/dashboard/scripts vcetne Swaggeru Zmeneno: - katalog konektoru uz neni jen staticky seznam. Akce ze skriptu se domeruji prekryvem v src/data/connectors.ts, takze se naraz objevi ve validaci stromu, ve vypoctu scope i v sablonach. Pri stejnem ID vyhrava skript - ConnectorOperation ma implementation a scriptId - ApiError na klientovi nese cele telo odpovedi a umi z nej vytahnout issues - Dockerfile kopiruje scripts/ do vysledneho image Ukladani nemuze rozbit fungujici skript: kod se nejdriv zapise do docasneho souboru, ten se nacte a overi, a az pak prepise puvodni. K tomu tri dokumenty navrhu dalsich kroku: 09 datove modely a prava, 10 runtime a rozpocet na 150 klientu, 11 popis skriptu konektoru. Overeno: npm run typecheck prochazi na serveru i webu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
49 KiB
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.
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.
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:
typeId: string | null; // null = ticket bez typu, jako dnes
fields: Record<string, string | number | boolean | null>;
Dve id u pole nejsou zbytecna. Je to totez rozdeleni, ktere uz v projektu je
a je vysvetlene v 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
visibleWhennebo 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.
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<string, string> }
/** 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<string, string> };
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:
"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.
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.
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:<id>. 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:
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
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 jinehoplatformAdmin, - token plati nejvyse 30 minut a neda se obnovit, v payloadu nese
acts 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.
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<string, unknown>;
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.
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:
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, neticketCount, 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:
- rozlozeni uzivatele, kdyz si ho upravil,
- jinak rozlozeni pro jeho roli ve firme, kdyz ho admin nastavil,
- 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 NULLna kazde business tabulce, indexy zacinajitenant_id.- Povinny argument
tenantIdsv 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 RESTRICTu vsecho, co nese historii. Smazany resitel dnes ticket neshodi, to chovani musi zustat.
Poradi prevodu
tenants,users,roles,people- identita, nejmensi objem, nejvic zavislosti.tickets,ticket_trace,ticket_types,ticket_tags,ticket_actions.connector_defs,connector_grants,connections,scripts- bod 9.automations,runs,run_steps,jobs- stromy a runtime.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_notifyma limit 8000 bajtu. Posilat jen typ a ID, ne cela data. Nevadi to,useApiQueryuz 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:
| { 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_versionv tabulcerun). 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:
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/previews 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+<token>@...) 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 |
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<string, unknown>;
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:
{ kind: 'action'; connectorId: string; operationId: string;
connectionId: string | null; // null = vychozi napojeni firmy pro tento konektor
inputs?: Record<string, string> }
Proc oboji a proc nullable:
- Explicitni
connectionIdje potreba, kdyz firma ma dva ucty teze sluzby. nulljako 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
keyVersionu kazdeho radku, aby sla vymena klice. - Pole jsou jen pro zapis. API umi
clientSecretnastavit a nikdy ho nevrati. Odpoved nese{ "clientSecret": { "isSet": true } }, ne hodnotu. To je presne to, coAGENTS.mdzakazuje: 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:
interface HttpImplementation {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
path: string; // sablona nad inputs a config
headers: Record<string, string>; // sablony, tajemstvi jen odkazem
body: string | null; // sablona
/** Mapovani odpovedi na outputFields: cesta v JSONu na nazev vystupu. */
outputMap: Record<string, string>; // { 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.
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:vmneni bezpecnostni hranice a nesmi se pouzit. Budisolated-vm, nebo QuickJS ve WASM.- Limity: CPU radove desitky ms, pamet jednotky MB, velikost vystupu, zadne
require, zadnefetch, 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:<id> |
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. 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.