Revize projektu: prava, vykon, runtime, portal a ARES

Prava a bezpecnost: spravce firmy uz nemuze nastavit priznak spravce
platformy ani clenstvi v cizi firme; pozvanky, konektory a automatizace
kontroluji sve pravo; cizi firma v query je 404; zivy stream posila
udalosti jen firmam, kterych se tykaji; akce nad ticketem maji kontrolu
prava za firmu ticketu a strop viditelnosti; tokeny se nelogujou; limit
pokusu na prihlaseni, kontakt a pozvanky; bezpecnostni hlavicky;
zachyceni chyb v async handlerech; timing-safe porovnani tokenu.

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

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

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

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-09-09 10:26:07 +02:00
co-authored by Claude Fable 5.1
parent 0c405ea55a
commit 104ae36783
215 changed files with 13226 additions and 8320 deletions
+52 -7
View File
@@ -86,6 +86,38 @@ s tim, co uzivatel smi. Dvoji vypocet se jednou rozejde.
firmu jako povinny argument. Clovek muze byt spravce v jedne firme a bezny
uzivatel v druhe.
**Pravo se kontroluje u kazde route, za firmu zaznamu.** Clenstvi ve firme
neni pravo. Konektor chce `connector.manage`, automatizace `automation.edit`,
uzivatele `user.manage` a jen ve sve firme, firmy jen spravce platformy.
Vestavene akce na ticketu jdou pres `builtinAction` v `ticketActions.ts`, kde
se pravo pta za firmu **toho ticketu** a ticket se nejdriv hleda pres strop
viditelnosti. Pristup se pocita jednou na request (`attachAccess`,
`req.access`) a routy si firmu berou pres `tenantOrDeny`.
**Udalost nese firmu.** `publish(kind, message, payload, tenantId)` ma
`tenantId` povinny a posledni, `null` znamena cela platforma. Stream ji
pouziva k filtru, takze udalost bez firmy by videl kazdy. Zmena entity
v nastaveni publikuje `<druh>.created|updated|deleted` s celym zaznamem, aby
klient nemusel po kazde udalosti znovu nacitat.
**Ciselniky ma klient ve skladu, provozni data ne.** Lide, skupiny, typy,
sluzby, konektory a pristup se berou z `lib/collections.tsx`
(`useCollection`), ktery se opravuje z udalosti. Tickety, behy a statistiky
zustavaji `useApiQuery` se strankovanim - jsou velke, meni se porad a strop
viditelnosti pocita server. Nova stranka nema volat `apiFetch` na ciselnik
primo.
**Opakuje se jen to, co samo rekne `retryable`.** Vychozi je ne. Chyba
spojeni, timeout pred odeslanim, 5xx a 429 ano; 401, 403, 404, spatny vstup,
`ctx.fail` a chyba v kodu ne - ty konci hned a zakladaji incident. Timeout uz
odeslaneho volani MCP se neopakuje, protoze MCP nema idempotencni klic.
Pravidlo je nad `StepResult` v `executor.ts` a plati pro kazdy novy druh kroku.
**Typ odpovedi API je jednou, v `src/shared`.** Web ho nekopiruje, bere ho
pres alias `@shared/*`; `web/src/types/` je jen fasada. Kopie na klientovi se
rozejde se serverem a prekladac to nepozna. Prevod je hotovy: 16 modulu
v `src/shared` vcetne uctu (`users.ts`), web nema zadny vlastni typ API.
## Jak vznika operace, kterou clovek vybere v builderu
Tri cesty, kazda ma svuj duvod:
@@ -131,14 +163,16 @@ tam je operace vlastnost sluzby, ne napojeni.
## Uloziste ma tri rezimy
| Rezim | Kdy | Prezije |
| ---------- | ----------------------------------- | -------------------- |
| `postgres` | je `DATABASE_URL` i `SECRETS_KEY` | vse |
| `file` | neni databaze, ale je datova slozka | restart, ne redeploy |
| `memory` | ani jedno | nic |
| Rezim | Kdy | Prezije |
| ---------- | ------------------------------------------------------ | -------------------- |
| `postgres` | je `DATABASE_URL`, migrace prosly a je cim sifrovat | vse |
| `file` | neni databaze, ale je datova slozka | restart, ne redeploy |
| `memory` | ani jedno | nic |
**Rozhodnuti je na jednom miste** (`connectorStore.ts`). Kdyby se rozlezlo po
kodu, jedno misto by se zapomnelo a chovalo by se pak jinak nez zbytek.
**Rozhodnuti je na jednom miste** (`initStores` v `store/index.ts`) a plati
pro vsechna uloziste vcetne konektoru. Kdyby se rozlezlo po kodu, jedno misto
by se zapomnelo a chovalo by se pak jinak nez zbytek - presne to se stalo,
kdyz mely konektory vlastni rozhodnuti.
Nasazeni bezi v rezimu `file`. Ma to dusledek, ktery je poznat az pri zatezi:
**kazda zmena prepisuje celou kolekci** jako formatovany JSON. U ticketu, ktere
@@ -167,6 +201,17 @@ ho napsal.
**Novy text v portalu** patri do `web/src/i18n/cs.ts` a klic do `en.ts`.
Anglictina je `Partial`, takze nemusi byt uplna - co chybi, spadne na cestinu.
**Novy typ odpovedi** patri do `src/shared`, web ho nekopiruje.
**Nova route** vznika pres `safeRouter`, ne `Router()`, aby odmitnuta promise
skoncila jako 500 a ne padem procesu. Chyba validace jde pres
`validationError`, aby mela `issues` ve stejnem tvaru jako zbytek. Kazde
volani ven (HTTP, MCP, SMTP, ARES) cte telo pres `src/net/guard.ts`.
**Novy formular** sklada `Field`, `Input`, `Select`, `Textarea`
z `components/ui/form/` a odeslani pres `useSubmit`. Vlastni tridy vstupniho
pole ve strance jsou to, co se prave odstranilo z patnacti mist.
**Novy sloupec** znamena novou migraci **a** doplneni obou uloziste,
souborového i databazoveho. Rozhrani je jedno, implementace dve.
+32 -19
View File
@@ -67,6 +67,16 @@ React aplikaci ze slozky `dist/public`.
| Helpdesk pro zadavatele | hotovo | pozadavek vidi zadavatel i resitel, kazdy ze sve strany |
| Odesilani e-mailu pres SMTP | hotovo | konektor se schrankou firmy, HTML telo s promennymi |
| Odesilani e-mailu z formulare | chybi | poptavka se zatim jen loguje |
| Jeden rezim uloziste | hotovo | `initStores` rozhoduje pro vsechna uloziste naraz |
| Zapisy serazene za sebou | hotovo | `withMirror` radi zapisy tehoz zaznamu, audit se oreza |
| Worker jako pool | hotovo | ctyri behy naraz nezavisle, tlukot, opakovani jen kdyz ma smysl |
| Prava za firmu u kazde route | hotovo | firmy, uzivatele, konektory, automatizace, akce ticketu |
| Udalosti za firmu | hotovo | stream filtruje, entity hlasi vznik, zmenu a smazani |
| Strankovani ticketu a behu | hotovo | `limit`, `offset`, `X-Total-Count` |
| Klientsky sklad ciselniku | hotovo | lide, skupiny, typy, sluzby, konektory, pristup |
| Formularova vrstva | hotovo | `ui/form`, `useSubmit`, `options`, jedna sada trid |
| Firma z registru ARES | hotovo | IC nebo nazev, statutari jako ucty, jen spravce platformy |
| Sdilene typy `src/shared` | hotovo | web je re-exportuje pres `@shared/*`, nic nekopiruje |
## Znama omezeni
@@ -74,17 +84,24 @@ Data prezijou restart, ale ne redeploy, kdyz neni databaze. Rezim se pozna
v portalu i v `/health/ready` a rozhoduje o nem jedno misto, viz
[14-databaze.md](14-databaze.md):
| Rezim | Kdy | Nasledek |
| ---------- | ---------------------------------- | ---------------------------- |
| `postgres` | je `DATABASE_URL` a migrace prosly | data se neztraci |
| `file` | neni databaze, je `DATA_DIR` | prezije restart, ne redeploy |
| `memory` | neni ani `DATA_DIR` | ztrati se pri restartu |
| Rezim | Kdy | Nasledek |
| ---------- | ------------------------------------------------- | ---------------------------- |
| `postgres` | je `DATABASE_URL`, migrace prosly a je cim sifrovat | data se neztraci |
| `file` | neni databaze, je `DATA_DIR` | prezije restart, ne redeploy |
| `memory` | neni ani `DATA_DIR` | ztrati se pri restartu |
Beh automatizaci uz existuje, ale je **synchronni v requestu**: webhook ceka,
nez cely strom dobehne, a pri padu procesu se rozdelany beh ztrati. Neni fronta
ani opakovani. Co to znamena pro vetsi provoz a co s tim, je zmerene
v [19-kapacita-200-firem.md](19-kapacita-200-firem.md), navrh fronty
v [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md).
Rezim je jeden pro vsechna uloziste vcetne konektoru. Driv se mohlo stat, ze
konektory jely z databaze a tickety ze souboru.
Beh automatizaci jde pres frontu a worker: webhook odpovi 202 a strom se
vykona na pozadi, pri chybe se opakuje jen to, co muze pominout. Fronta je ale
**v pameti jednoho procesu**: vic instanci by si vzalo tentyz beh, nad
Postgresem to chce `SKIP LOCKED`. Popis je
v [20-fronta-a-runtime.md](20-fronta-a-runtime.md), rozbor kapacity
v [19-kapacita-200-firem.md](19-kapacita-200-firem.md).
Limity requestu (prihlaseni, kontakt, pozvanky) jsou v pameti jedne instance,
stejne jako stream. Pri vice instancich by kazda pocitala zvlast.
Obsah verejneho webu je ukazkovy. Nazev firmy, reference, tym i cisla jsou
vymyslene a pred ostrym pouzitim se musi nahradit. Firemni udaje jsou na jednom
@@ -99,21 +116,17 @@ co ukazat i na prazdne instanci.
## Dalsi krok
Nejuzitecnejsi pristavek je runtime. Strom uz nese vsechno potrebne: spoustec
s parametry, podminky a u ticketu i kanalu nastavena pole se sablonami. Chybi
jen to, co ho vykona. Do te doby je ulozena automatizace popis zameru, ne provoz.
Runtime je hotovy: fronta, worker jako pool, opakovani jen u chyb, ktere
mohou pominout, incident z koncove chyby. Dalsi krok je **vic instanci**, tedy
vyber z fronty nad Postgresem se `SKIP LOCKED`, sdileny kanal pro stream
a sdileny citac limitu requestu.
Vedle toho zbyva prevest na `inputs` i ostatni konektory a doplnit odkazy
na vystup predchoziho kroku, ne jen na spoustec. Podrobnosti
Vedle toho zbyva prevest na `inputs` i ostatni konektory. Podrobnosti
v [05-dashboard-a-builder.md](05-dashboard-a-builder.md).
Za rozmysleni stoji evidence bugs a wishes. Zamerne to nejsou tickety,
duvod je v [06-tickety.md](06-tickety.md).
Prvni cast navrhu uz je hotova: vykonna cast konektoru, tedy skripty
s manifestem a kontrolou parametru, viz [11-skripty-konektoru.md](11-skripty-konektoru.md).
Runner je pripraveny, chybi nad nim fronta.
Datove modely a prava z [09-navrh-rozsireni.md](09-navrh-rozsireni.md) jsou
hotove, popis stavu je v [17-nastaveni-a-prava.md](17-nastaveni-a-prava.md).
Navrh k rozhodnuti zustava [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md)
+54 -15
View File
@@ -31,28 +31,44 @@ image jen `dist`, takze staci jedna slozka.
| `src/config.ts` | cteni environment variables, normalizace `ROOT_PATH` |
| `src/openapi.ts` | OpenAPI definice vcetne `servers` s prefixem proxy |
| `src/types.ts` | typy uzivatele a JWT payloadu |
| `src/middleware/auth.ts` | `requireAuth`, `requireRole` |
| `src/events/bus.ts` | sbernice udalosti, ze ktere cerpa SSE stream |
| `src/shared/` | ciste typove moduly API, jediny zdroj typu pro server i web |
| `src/middleware/auth.ts` | `requireAuth`, `requireRole`, `requirePlatformAdmin` |
| `src/middleware/asyncHandler.ts` | `wrap`, `safeRouter`: odchyceni odmitnute promise v handleru |
| `src/middleware/tenant.ts` | `attachAccess`, `tenantOrDeny`, `scopeOrDeny`: firma requestu na jednom miste |
| `src/middleware/rateLimit.ts` | limit requestu v pameti, 429 s `Retry-After` |
| `src/middleware/validation.ts` | `validationError`, jeden tvar chyby validace |
| `src/lib/secure.ts` | `timingSafeEqualString` pro tokeny v adrese |
| `src/net/guard.ts` | kontrola adresy, cteni tela s limitem, popis chyby site - pro vsechno, co vola ven |
| `src/events/bus.ts` | sbernice udalosti, ze ktere cerpa SSE stream, udalost nese firmu |
| `src/routes/auth.ts` | prihlaseni, odhlaseni, kdo jsem |
| `src/routes/dashboard.ts` | data portalu, katalog konektoru, CRUD automatizaci |
| `src/routes/stream.ts` | SSE stream zmen |
| `src/routes/simulate.ts` | vyvolani provoznich udalosti |
| `src/routes/dashboard.ts` | data portalu, tickety, behy, CRUD automatizaci |
| `src/routes/ticketActions.ts` | akce nad ticketem vcetne vestavenych, pravo za firmu ticketu |
| `src/routes/settings.ts` | CRUD entit pres `crud.ts`, uzivatele, ARES |
| `src/routes/ares.ts` | firma z registru ARES, jen spravce platformy |
| `src/routes/connectors.ts` | konektory firmy, overeni, nastroje MCP |
| `src/routes/stream.ts` | SSE stream zmen, filtr podle firem uzivatele |
| `src/routes/webhook.ts` | verejny prijem dat do automatizace |
| `src/routes/contact.ts` | poptavkovy formular z webu |
| `src/ares/client.ts` | klient verejneho API ARES |
| `src/data/store/` | tri rezimy uloziste, `withCache`, `withMirror`, `initStores` |
| `src/data/snapshot.ts` | atomicky zapis JSONu pro rezim `file` |
| `src/data/ticketStore.ts` | tickety, jejich resitele, log prubehu, prehled vytizeni |
| `src/data/people.ts` | resitele ticketu - oddeleni od uzivatelu portalu |
| `src/data/tenants.ts` | firmy, ktere portal pouzivaji |
| `src/data/tenants.ts` | firmy, ktere portal pouzivaji, vcetne udaju z ARES |
| `src/data/access.ts` | kdo co vidi - jedno misto pro cely portal |
| `src/data/widgets.ts` | katalog widgetu prehledu |
| `src/data/dashboardLayouts.ts` | rozlozeni dashboardu za dvojici uzivatel a firma |
| `src/data/incidentStore.ts` | incidenty vcetne zmen a udalosti |
| `src/data/incidentStore.ts` | incidenty vcetne zmen a udalosti, filtr na firmu povinny |
| `src/data/automationStore.ts` | automatizace, strom akci, tokeny webhooku |
| `src/data/connectors.ts` | katalog konektoru, jejich spousteču a akci |
| `src/data/services.ts` | katalog sluzeb, jejich spousteču a akci |
| `src/data/conditions.ts` | typy parametru a operatory podminek |
| `src/data/templates.ts` | sablony `{{parametr}}` v nastaveni kroku |
| `src/data/flowScope.ts` | co je videt v kterem miste stromu |
| `src/data/users.ts` | demo uzivatele |
| `src/data/users.ts` | uzivatele portalu, demo ucty |
| `src/data/mock.ts` | souhrn pro prehled a casova rada grafu |
| `src/runtime/` | fronta, worker, executor stromu, vestavene kroky, sandbox skriptu firmy |
| `src/scripts/` | skripty konektoru: registr, runner, HTTP, pomocne funkce |
| `src/mcp/` | klient MCP, prihlaseni, dialekty, `errors.ts` se spolecnou chybou prihlaseni |
## Mapa kodu - web
@@ -62,16 +78,25 @@ image jen `dist`, takze staci jedna slozka.
| `web/src/App.tsx` | routovani, portal se nacita lazy |
| `web/src/index.css` | design tokeny a vlastni utility Tailwindu |
| `web/src/config/brand.ts` | vsechny firemni udaje na jednom miste |
| `web/src/lib/api.ts` | fetch wrapper, sprava tokenu, skladani adres |
| `web/src/lib/api.ts` | fetch wrapper, sprava tokenu, skladani adres, `auth:expired` na 401 |
| `web/src/lib/eventStream.ts` | cteni SSE streamu pres fetch |
| `web/src/lib/useApiQuery.ts` | nacitani dat vcetne obnoveni pri udalosti |
| `web/src/lib/useApiQuery.ts` | nacitani dat, cache, spolecny debounce, `refreshing` misto odmontovani |
| `web/src/lib/collections.tsx` | klientsky sklad ciselniku za firmu, opravovany z udalosti |
| `web/src/lib/ticketEvents.ts` | oprava seznamu ticketu z `payload.ticket` bez dotazu |
| `web/src/lib/useSubmit.ts` | odeslani formulare: `saving`, chyba, reset na jednom miste |
| `web/src/lib/options.ts` | pevne ciselniky (priority) |
| `web/src/lib/useUnsavedChanges.ts` | varovani pri odchodu z rozepsaneho formulare |
| `web/src/lib/flow.ts` | ciste funkce nad stromem automatizace |
| `web/src/components/dashboard/` | shell portalu, dlazdice, graf, stream, simulace |
| `web/src/components/dashboard/flow/` | strom akci, vyber kroku, nastaveni poli akce |
| `web/src/types/` | fasada nad `src/shared` (alias `@shared/*`), zadne vlastni typy API |
| `web/src/components/ui/` | zakladni prvky, `Chip` |
| `web/src/components/ui/form/` | `Field`, `Input`, `Select`, `Textarea`, `controlClass`: jedna sada trid |
| `web/src/components/dashboard/` | shell portalu, dlazdice, graf, stream, `TicketCard` |
| `web/src/components/dashboard/flow/` | strom akci: `FlowCanvas` a karty `ActionCard`, `ConditionCard`, `ForeachCard`, `StepControls` |
| `web/src/components/dashboard/TicketTrace.tsx` | log ticketu jako strom |
| `web/src/components/dashboard/TicketWorkload.tsx` | prehled, kdo co ma u sebe |
| `web/src/components/home/` | sekce homepage |
| `web/src/pages/` | jedna stranka je jeden soubor |
| `web/src/pages/dashboard/connectors/` | casti stranky Konektory: karta, editor, log, nastroje |
## Klicova rozhodnuti
@@ -88,8 +113,22 @@ Cenou je rucni parsovani a rucni znovupripojeni v `web/src/lib/eventStream.ts`.
**Ceske cesty v URL.** `/sluzby`, `/o-nas`, `/prihlaseni`, `/dashboard/tickety`.
Kod zustava anglicky.
**Data v pameti.** Vedome zjednoduseni prototypu. Uloziste jsou oddelena od rout,
takze napojeni na databazi znamena prepsat soubory v `src/data/`, ne endpointy.
**Uloziste je za rozhranim.** Tri rezimy (Postgres, soubor, pamet), jedno
rozhrani a **jedno rozhodnuti** v `initStores`. Routy nevedi, ktery rezim jede.
Podrobnosti v [14-databaze.md](14-databaze.md).
**Typy API jsou jednou.** Ciste typove moduly v `src/shared` ctou server i web,
web pres alias `@shared/*`. Kopie typu na klientovi se jednou rozejde se
serverem a prekladac to nepozna. `web/src/types/` je jen fasada, ktera je
re-exportuje; ucet uzivatele bere i `AuthContext` ze `@shared/users`.
**Kazdy async handler je odchyceny.** Routy vznikaji pres `safeRouter`,
odmitnuta promise skonci jako 500 s logem, ne padem procesu. Bez toho stacil
jeden zapomenuty `try` a AppFactory restartovala container.
**Firma requestu se pocita jednou.** `attachAccess` da do `req.access`, co
uzivatel smi, a `tenantOrDeny` z toho odvodi firmu. Kazda route, ktera si to
pocitala sama, to delala trochu jinak.
**Filtr na firmu je povinny argument.** `listTickets`, `listPeople`
i `listAutomations` vyzaduji `tenantIds`. Zapomenuty filtr tak neznamena "vse",
+93 -11
View File
@@ -85,6 +85,9 @@ Vyzaduji `Authorization: Bearer <token>`:
| GET | `/api/dashboard/settings/people-overview` |
| GET | `/api/dashboard/settings/users-overview` |
| PATCH | `/api/dashboard/settings/users/:id/password` |
| GET | `/api/dashboard/settings/ares/companies` |
| GET | `/api/dashboard/settings/ares/companies/:ico/persons` |
| POST | `/api/dashboard/settings/ares/tenants` |
| POST | `/api/admin/impersonate` |
| POST | `/api/admin/impersonate/stop` |
| GET | `/api/admin/impersonate/candidates` |
@@ -102,20 +105,35 @@ fabrika (`src/routes/crud.ts`):
Jednotny pro cele API:
```json
{ "error": "validation_error", "message": "Zadejte platny e-mail." }
{
"error": "validation_error",
"message": "Zadejte platny e-mail.",
"issues": [{ "field": "email", "message": "Zadejte platny e-mail." }]
}
```
| HTTP | `error` | Kdy |
| ---- | --------------------- | ------------------------------------------- |
| 400 | `validation_error` | vstup neprosel schematem |
| 400 | `validation_error` | vstup neprosel schematem, `issues` po polich |
| 401 | `unauthorized` | chybi nebo neplatny token |
| 401 | `invalid_credentials` | spatny e-mail nebo heslo |
| 403 | `forbidden` | nedostatecna role |
| 404 | `not_found` | zaznam nebo endpoint neexistuje |
| 403 | `forbidden` | nedostatecne pravo v dane firme |
| 404 | `not_found` | zaznam nebo endpoint neexistuje, nebo je cizi firmy |
| 409 | ruzne | operace nedava v danem stavu smysl |
| 429 | `too_many_requests` | prekrocen limit requestu, hlavicka `Retry-After` |
| 500 | `internal_error` | neodchycena chyba, detail jen mimo produkci |
`message` je vzdy cesky a je urcena k zobrazeni uzivateli.
`message` je vzdy cesky a je urcena k zobrazeni uzivateli. Chybu validace
sklada `validationError` v `src/middleware/validation.ts`, aby `issues` mely
vsude stejny tvar a formular umel chybu ukazat u pole.
Limity (`src/middleware/rateLimit.ts`) jsou jen na verejnych endpointech, kde
se da hadat: prihlaseni 20 pokusu za 15 minut, kontakt 5 za hodinu, prijeti
pozvanky 5 za 15 minut. Pocita se podle adresy klienta, proto ma Express
`trust proxy` = 1 - bez toho by vsichni za Caddy sdileli jeden limit.
Kazdy asynchronni handler je obaleny (`safeRouter` v `src/middleware/asyncHandler.ts`).
Odmitnuta promise je 500 s logem, ne pad procesu.
## Autentizace
@@ -139,6 +157,21 @@ Typy udalosti: `ticket.created`, `ticket.updated`, `ticket.assigned`,
`automation.created`, `automation.updated`, `automation.deleted`,
`automation.run`, `webhook.received`.
K tomu udalosti entit `tenant`, `user`, `role`, `person`, `group`,
`ticketType`, `action`, `widget`, `connector`, `feature` s priponou
`.created`, `.updated`, `.deleted`. Payload je `{ id, <druh>: zaznam }`,
u smazani jen `{ id }`. Publikuje je `crudRouter` (volba `event`), routy
konektoru a PUT features. Klient z nich opravuje sklad ciselniku bez dotazu.
**Kazda udalost nese `tenantId`** (`null` = cela platforma). Stream posila jen
udalosti firem, do kterych uzivatel patri, a to i v historii po pripojeni.
Udalost s `payload.userId` jde jen tomu cloveku. Spravce platformy vidi vse.
Driv videl kazdy prihlaseny udalosti vsech firem - nazev ticketu cizi firmy
v bubline je unik dat, i kdyz se na ticket nedostane.
`ticket.updated`, `ticket.assigned` a `ticket.resolved` nesou
v `payload.ticket` cely ticket, aby klient opravil seznam na miste.
Klient se pripojuje pres fetch s hlavickou `Authorization`, ne pres EventSource.
Duvod je v [03-architektura-a-mapa-kodu.md](03-architektura-a-mapa-kodu.md).
@@ -166,8 +199,9 @@ curl -X POST https://services.csbot.cz/apps/<app-id>/webhook/<token> \
-d '{"customer":"Nordis","score":18}'
```
Prototyp pozadavek prijme, zvaliduje a zapocita do metrik, ale strom akci
nevykona - runtime neexistuje.
Token se porovnava v konstantnim case (`timingSafeEqualString`
v `src/lib/secure.ts`), stejne jako token prijmu a kod pozvanky. V logu
requestu je z tokenu videt jen prvnich sest znaku.
## Firmy a pohledy
@@ -192,6 +226,11 @@ bez vysvetleni.
Odpoved nese vedle `items` jeste `meId`. Klient podle nej pozna, ktere tickety
jsou jeho, a jestli ma vubec smysl nabizet filtr "moje".
**Strankovani.** `/tickets` a `/runs` berou `limit` a `offset`, `limit` nejvys
500. Celkovy pocet je v hlavicce `X-Total-Count`, u ticketu i v tele jako
`total`. Bez `limit` se vraci vse jako driv (u behu poslednich 50), aby se
nerozbily stavajici odkazy. Klient cte hlavicku pres `apiFetchWithMeta`.
Filtrovani dela **server**, ne klient. Seznam a prehled vytizeni tak nikdy
neukazuji jina cisla. Vyhledavaci pole v portalu je jina vec - to jen dohledava
v uz nactenem seznamu.
@@ -246,7 +285,26 @@ selhala**. Selhani akce neni chyba API. V odpovedi je `ok`, `summary`, `detail`
s celym chybovym hlasenim a `durationMs`. Cely prubeh se zapise do logu ticketu.
Vestavene akce (`type`, `tags`, `group`, `assign`, `status`, `comment`) jsou
zvlast: meni ticket sam, ne cizi sluzbu, a kazda ma vlastni pravo.
zvlast: meni ticket sam, ne cizi sluzbu, a kazda ma vlastni pravo. Vsechny
vcetne `claim` jdou pres `builtinAction` v `src/routes/ticketActions.ts`, kde
se pravo pta za firmu ticketu a ticket se nejdriv najde pres strop
viditelnosti (`visibleTicketOrDeny`). Driv mely `assign`, `status` a `comment`
vlastni handlery a kazdy se ptal jinak.
## Firma z registru ARES
Jen spravce platformy (`/api/dashboard/settings/ares`). Popis rozhodnuti je
v [07-firmy-a-prava.md](07-firmy-a-prava.md).
| Endpoint | Co vraci |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `GET ares/companies?query=` | same cislice (1 az 8) hledaji IC presne, jinak nazev, nejvys 10. U firmy, ktera uz v portalu je, `existingTenantId` |
| `GET ares/companies/{ico}/persons` | soucasni statutari a prokura z verejneho rejstriku, u kazdeho navrzeny e-mail `IC-poradi@placeholder.cz` |
| `POST ares/tenants` | zalozi firmu a ucty vybranych osob, vraci firmu a seznam uctu |
Chyba registru je `ares_error` s kodem podle toho, co ARES vratil - neni to
chyba naseho API a nema se opakovat automaticky. Adresa registru je
`ARES_BASE_URL`.
## Prijem udalosti do ticketu
@@ -284,9 +342,33 @@ a kdy se to zkusi znovu.
## Prava a navigace
`GET /api/dashboard/access` vraci `permissions` (efektivni prava po slouceni
roli), `nav` (zalozky, ktere ma volajici videt) a `platformAdmin`. Klient podle
toho kresli, ale **nic si nedovozuje** - kdo co smi, rozhoduje server u kazdeho
requestu znovu.
roli), `nav` (zalozky, ktere ma volajici videt), `platformAdmin` a `roleNames`
(nazvy roli v prepnute firme, pro popisek u uctu). Klient podle toho kresli,
ale **nic si nedovozuje** - kdo co smi, rozhoduje server u kazdeho requestu
znovu.
### Kdo co smi, po routach
Pravo se vzdy pta **za firmu zaznamu**, ne za prepnutou firmu. Cizi firma je
404, chybejici pravo 403.
| Co | Pravo |
| ----------------------------------------------- | --------------------------------------------------------------------- |
| firmy CRUD, ARES | spravce platformy |
| uzivatele CRUD | spravce platformy, nebo `user.manage` jen v ramci sve firmy |
| pozvanky | `user.manage`, role jen z te firmy |
| konektory create, update, delete, test | `connector.manage` |
| automatizace create, update, delete, regenerate | `automation.edit` |
| `/services`, `/connectors/services` | clenstvi ve firme |
| assign, status, comment, claim na ticketu | prava vestavene akce za firmu ticketu plus strop viditelnosti |
| `/api/admin/impersonate*` | `impersonate` |
| `/api/admin/audit` | `audit.view` |
| `/storage`, `/scripts` s cestami na serveru | cesty jen spravci platformy, ostatni dostanou odpoved bez nich |
Spravce firmy s `user.manage` **nenastavi `platformAdmin`**, neprida clenstvi
v jine firme, nesahne na spravce platformy a nesmaze cloveka, ktery je i
v jine firme - ten ucet neni jen jeho. Podrobnosti
v [07-firmy-a-prava.md](07-firmy-a-prava.md).
`GET /api/dashboard/settings/catalog` vraci katalog prav a modulu, aby formular
role nemel seznam prav napsany v kodu klienta.
+55 -8
View File
@@ -25,10 +25,49 @@ Portal drzi jedno SSE spojeni pro celou aplikaci. Zajistuje ho
- Prichozi udalosti ukazuje `EventToasts` jako bubliny vpravo dole.
- Data se obnovuji sama. `useApiQuery` ma volitelny `refetchOn` se seznamem typu
udalosti, po kterych se ma dotaz zopakovat. Vice udalosti tesne po sobe se
slouci do jednoho nacteni.
slouci do jednoho nacteni - debounce 150 ms je **spolecny pro vsechny
hooky**, takze jedna udalost je jedna vlna requestu, ne pet v peti chvilich.
Pri vypadku se stream znovu pripojuje s exponencialne rostoucim odstupem
az do 15 sekund, aby pri vypadku serveru neubijel provoz.
az do 15 sekund, aby pri vypadku serveru neubijel provoz. Na 401 a 403 se
pripojovat prestane: token vyprsel a klient vyvola `auth:expired`, po kterem
se portal odhlasi a Login rekne, ze prihlaseni vyprselo.
### Obnova neodmontuje stranku
`useApiQuery` vraci `loading` jen **do prvnich dat**, potom uz `refreshing`.
`DataState` pri `refreshing` nechava deti vykreslene a jen ukaze, ze bezi
obnova. Prvni verze prepnula `loading` pri kazde udalosti ze streamu a
`DataState` vykreslil spinner misto obsahu - pri behu automatizace se stranka
nekolikrat za sekundu odmontovala a namontovala, vcetne ztraty kurzoru
v rozepsanem poli.
```ts
useApiQuery<T>(path, { refetchOn?, body?, enabled?, patchOn? })
-> { data, loading, refreshing, error, total, reload }
```
Dotazy sdili cache modulu a deduplikaci bezicich requestu (klic je firma,
cesta a telo), takze dve komponenty se stejnym dotazem se ptaji jednou.
`patchOn` opravi data v cache primo z udalosti: `lib/ticketEvents.ts` bere
`payload.ticket` z `ticket.updated` a vymeni radek v seznamu bez dotazu.
### Ciselniky jsou v klientskem skladu
Lide, skupiny, typy ticketu, sluzby, konektory a pristup se nectou stranka po
strance, ale ze skladu `lib/collections.tsx`: `useCollection(key)`,
`useAccess()`, `useCollectionSelector`. Kolekce se nacte pri prvnim pouziti,
vymaze se pri prepnuti firmy a odhlaseni a **opravuje se z udalosti entit**
(`person.updated` a podobne): zaznam z payloadu se vlozi nebo smaze, a kdyz
payload zaznam nenese, nacte se ta jedna kolekce znovu.
Rozhodnuti majitele produktu je stredni cesta: ciselniky do skladu, **tickety,
behy a statistiky zustavaji dotazy na server** se strankovanim. Jsou velke,
meni se porad a strop viditelnosti pocita server - klientska kopie by je
ukazovala jinak nez prehled vytizeni.
Filtry seznamu ticketu jsou v URL. Nalez jde poslat kolegovi a tlacitko zpet
vrati predchozi filtr; driv byl filtr stav stranky a po obnoveni zmizel.
## Simulace provozu
@@ -102,6 +141,15 @@ Vetve ANO a NE jsou vedle sebe jen tehdy, kdyz je na to v dane karte misto.
Rozhoduje **sirka karty, ne sirka okna** - pouzivaji se container queries
(`@container` a `@2xl:grid-cols-2` v `FlowCanvas.tsx`).
Karty jsou rozdelene po druhu kroku: `flow/ActionCard.tsx`,
`ConditionCard.tsx`, `ForeachCard.tsx`, `StepControls.tsx`, spolecne typy
v `canvasTypes.ts`. `FlowCanvas.tsx` uz jen sklada. Karty jsou v `memo`,
callbacky se predavaji podle ID kroku a `collectScopes` je memoizovane -
u stromu o padesati krocich byl driv kazdy stisk klavesy v poli prekreslenim
vseho.
Odchod z rozepsaneho stromu hlida `lib/useUnsavedChanges.ts`.
Duvod: kazde zanoreni pulí dostupnou sirku. S beznym `lg:grid-cols-2` vypadal
strom na sirokem monitoru dobre v prvni urovni a ve treti uz mel karty siroke
par desitek pixelu, takze se popisy lamaly po jednom slove. Container query se
@@ -157,8 +205,10 @@ je vypise. Rozdelana prace se nikdy nezahazuje.
## Pridani konektoru
1. Pridat zaznam do `connectors` v `src/data/connectors.ts` vcetne `triggers`
a `actions`.
1. Pridat zaznam do katalogu v `src/data/services.ts` vcetne `triggers`
a `actions`. ID operace musi byt v ramci sluzby unikatni,
`checkOperationIds()` duplicitu pri nacteni zaloguje - druha by tise
prekryla prvni.
2. Pokud pouziva novou ikonu, doplnit klic do `web/src/lib/connectorIcons.ts`.
Musi existovat v `lucide-react`.
3. Pokud patri do nove kategorie, doplnit ji do `connectorCategories` a do typu
@@ -173,11 +223,8 @@ Builder i katalog ji vezmou automaticky.
| Chybi | Poznamka |
| ---------------------------- | --------------------------------------------------------- |
| `inputs` u zbylych konektoru | zatim ticket, kanaly, CRM a AI, ostatni maji jen `fields` |
| Vazba logu ticketu na beh | log plni simulace, ne vykonany strom |
| Kombinovane podminky | jedna podminka je jedno porovnani, AND a OR jen vnorenim |
| Beh automatizaci | ulozeny strom se nevykonava |
| Historie behu a logy | prazdne, chybi runtime |
| Drag and drop | presouvani je zatim tlacitky nahoru a dolu |
| Strankovani v portalu | server `limit` a `offset` umi, seznam ticketu si zatim bere vse |
## Co je videt v kterem kroku
+56 -5
View File
@@ -132,10 +132,19 @@ rozlezlo po routach, driv nebo pozdeji vznikne endpoint, ktery filtr zapomene.
"tenants": [{ "id": "tnt_automia", "name": "Automia" }],
"defaultTenantId": "tnt_automia",
"canAssignOthers": true,
"personId": "ppl_uhlir"
"personId": "ppl_uhlir",
"roleNames": ["Správce"]
}
```
Pristup se pocita **jednou na request** (`attachAccess`
v `src/middleware/tenant.ts`, vysledek v `req.access`) a routy si z nej berou
firmu pres `tenantOrDeny`. Kazda route, ktera si to pocitala sama, to delala
o neco jinak a jedna z nich spatne.
Stream udalosti tutez informaci pouziva k filtru: uzivatel dostane jen udalosti
svych firem, viz [04-api.md](04-api.md).
Klient podle toho kresli prepinac. **Nesmi si to dovozovat sam** - jinak by se
prava pocitala na dvou mistech a jednou se rozejdou.
@@ -197,15 +206,57 @@ Heslo je u vsech `demo1234`.
Druhy ucet je ten zajimavy: ukazuje prepinac firem i to, ze prava se lisi
podle toho, ktera firma je prave zvolena.
## Kdo koho zaklada
Rozhodnuti z revize v zari 2026: **firmy zaklada spravce platformy, lidi ve
firme spravuje spravce firmy.** Je to hranice mezi nasim pravem a zakaznickym
a drzi se vsude, kde se neco zaklada:
| Co | Kdo smi | Kde se to kontroluje |
| -------------------------- | ------------------------------------------------------------ | ---------------------------------------- |
| firma | jen spravce platformy (`platformOnly` u CRUD firem) | `src/routes/settings.ts` |
| firma z registru ARES | jen spravce platformy | `src/routes/ares.ts` |
| uzivatel | spravce platformy, nebo `user.manage` jen ve sve firme | `src/routes/settings.ts` |
| pozvanka | `user.manage`, role jen z te firmy | `src/routes/invites.ts` |
| konektor | `connector.manage` | `src/routes/connectors.ts`, `dashboard.ts` |
| automatizace | `automation.edit` za firmu automatizace | `src/routes/dashboard.ts` |
| akce nad ticketem | pravo akce za firmu ticketu a strop viditelnosti | `src/routes/ticketActions.ts` |
Spravce firmy s `user.manage` ma **jen svou firmu**: nenastavi `platformAdmin`,
neprida cizi clenstvi, nesahne na spravce platformy a nesmaze cloveka, ktery
je i v jine firme. Posledni bod neni formalita - ucet ve dvou firmach neni jen
jeho a smazat ho znamena vzit pristup i te druhe.
Driv u vetsiny rout stacilo clenstvi ve firme. Clen s roli `viewer` tak mohl
zalozit konektor nebo smazat automatizaci. Prava v katalogu byla, jen se na ne
routy neptaly.
### Firma z registru ARES
Zalozit firmu rucne znamena opsat nazev, IC, DIC a adresu a pak zvlast
zakladat ucty lidem, kteri ji povedou. Proto `Nastaveni`, firma z ARES:
spravce platformy zada IC nebo nazev, vybere firmu a z verejneho rejstriku
dostane **soucasne cleny statutarniho organu a prokuru**. Vybrani dostanou
ucet s roli `role_admin` v nove firme a nahodnym heslem.
Rejstrik e-maily nezna. Kazda osoba proto dostane zastupnou adresu
`IC-poradi@placeholder.cz`, pokud spravce nezada skutecnou. **Zastupna adresa
se musi nahradit** - s ni se clovek neprihlasi a nedostane pozvanku. Portal ji
pozna (`isPlaceholderEmail`), aby slo upozornit.
Firma nese `ico`, `dic`, `address` a `legalForm`; IC je unikatni a CRUD firem
to hlida, takze tataz firma nevznikne dvakrat. Hledani v ARES u uz zalozene
firmy vrati `existingTenantId` misto tlacitka Zalozit.
Od te chvile je to na spravci firmy: skupiny, vedouci, clenove, pozvanky.
Spravce platformy do firmy nesaha, pokud nemusi.
## Co chybi
| Chybi | Poznamka |
| ------------------------- | ------------------------------------------------ |
| Sprava clenstvi z portalu | memberships jdou zmenit jen v kodu |
| Pozvanky uzivatelu | zadny onboarding |
| Tenant u incidentu | incidenty jsou zatim spolecne, nefiltruji se |
| Tenant u konektoru | katalog je spolecny, napojeni se zatim neeviduje |
| Audit pristupu | odepreni se jen loguje, nikde se neuklada |
| Nahrada zastupnych adres | portal je pozna, ale nenuti spravce je vymenit |
## Strop viditelnosti
+16 -3
View File
@@ -19,8 +19,10 @@ Dokud se neklikne na Ulozit, nic se neuklada. Zrusit vrati puvodni rozlozeni.
Ne za uzivatele. Clovek ve dvou firmach chce v kazde videt neco jineho
a smichat mu to dohromady by bylo horsi nez zadne nastaveni.
Klic je `${userId}:${tenantId}`, uloziste je `src/data/dashboardLayouts.ts`.
Kdo si dashboard jeste neupravil, dostane vychozi rozlozeni a `custom: false`.
Klic je `${userId}:${tenantId}`, uloziste je `src/data/dashboardLayouts.ts`
(pres `withMirror`, prezije restart). Kdo si dashboard jeste neupravil,
dostane vychozi rozlozeni a `custom: false`. Ulozene rozlozeni si drzi
`createdAt` i po uprave.
## Katalog widgetu
@@ -54,6 +56,18 @@ deset stejnych dotazu na server.
Prehled drzi ctyri dotazy (souhrn, tickety, incidenty, vytizeni) a rozdava je
vsem widgetum. Dlazdice, ktera zadna data nepotrebuje, o nich proste nevi.
Totez plati na serveru: `POST /widget-data` nacte seznam ticketu **jednou na
request** a kazdy widget si z nej filtruje svoje. Driv sel `listTickets` za
kazdy widget zvlast, tedy desetkrat za otevreni prehledu.
Pri udalosti ze streamu se dotazy obnovi na pozadi (`refreshing`) a dlazdice
zustavaji vykreslene. Driv se cely prehled pri kazde udalosti odmontoval
a ukazal spinner, viz [05-dashboard-a-builder.md](05-dashboard-a-builder.md).
Ukazkove widgety "Moje tickety" a "Fronta bez resitele" filtruji
`closed: false`, ne podle nazvu stavu - stav je volny retezec a slovnik
`defaultStatuses` je cesky (Novy, V reseni, Ceka na klienta, Vyreseno).
## Mrizka
Sest sloupcu, sirky mapuji na `col-span`: tretina 2, polovina 3, cela 6.
@@ -96,4 +110,3 @@ Nabidka i rozlozeni ho vezmou automaticky.
| Nastaveni jednotlivych widgetu | napr. kolik radku ukazat, za jake obdobi |
| Vlastni metriky | katalog je pevny, nejde si nadefinovat vlastni |
| Sdilene rozlozeni pro firmu | kazdy si upravuje jen to svoje |
| Databaze | rozlozeni je v pameti, restart je vrati na vychozi |
+46 -4
View File
@@ -132,7 +132,7 @@ export async function run(inputs, ctx) { /* ... */ }
| `ctx.http` | `get`, `post`, `patch`, `put`, `del`, `postForm` nad adresou napojeni |
| `ctx.util` | pomocne funkce, viz nize |
| `ctx.log` | radek do logu behu, vzdy zredigovany a zkraceny |
| `ctx.config` | necitliva cast nastaveni napojeni |
| `ctx.config` | necitliva cast nastaveni napojeni, tajna pole **nikdy** (`scriptConfig`) |
| `ctx.idempotencyKey` | stabilni pres vsechny pokusy tehoz kroku |
| `ctx.fail` | koncova chyba, neopakuje se |
| `ctx.retry` | docasna chyba, ma smysl zkusit znovu |
@@ -175,6 +175,20 @@ z nich by to resil spatne.
| `text`, `num`, `bool`, `date` | prevody s fallbackem |
| `round(value, decimals)` | zaokrouhleni, uctuje se v halerich |
| `need(value, label)` | vrati hodnotu, nebo skonci citelnou chybou |
| `day(value)` | datum bez casu, `YYYY-MM-DD`, jinak prazdny retezec |
| `list(body, ...names)` | seznam z odpovedi: pole primo, nebo pod danym klicem, `data`, `content`, `items`, `results`. Jinak `null` |
| `addresses(value)` | adresy z pole "Prijemci" oddelene carkou nebo strednikem, bez prazdnych |
| `quote(value)` | hodnota v jednoduchych uvozovkach pro filtr OData nebo SQL, apostrof zdvojeny |
Ctyri posledni pribyly v zari 2026, kdyz se ukazalo, ze osm skriptu ma kazdy
svou verzi. Sablona `scripts/_sablona.js` je vsechny vypisuje, aby se nehledaly
v kodu serveru.
`ctx.config` je slozene z `scriptConfig` v `src/scripts/connections.ts`:
z nastaveni napojeni vynecha kazde pole, jehoz hodnota je mezi tajnymi. Skript
tedy heslo SMTP ani tajemstvi OAuth nedostane ani omylem, i kdyz je runtime
(SMTP, MCP) potrebuje - ty si je berou z `serviceConfig`, ke kteremu skript
nema pristup.
## Chyby: opakovatelne a koncove
@@ -225,6 +239,20 @@ chyby, projde nahradou znamych tajnych hodnot za hvezdicky.
Neni to volitelne dolazeni, je to soucast zapisu.
Redaktor (`createRedactor`) maskuje tajemstvi ve **ctyrech tvarech**:
`Bearer abc`, hole `abc`, URL-encoded a JSON-escaped. Cizi sluzby vraceji
prijaty token v chybe i uvnitr adresy nebo v zaescapovanem JSONu, a tam by
hola hodnota nesedela.
Zkracovani ma jednu konstantu, `DETAIL_BYTES` v `src/scripts/util.ts`
(`SCRIPT_ERROR_DETAIL_BYTES`, vychozi 8 kB). Detail chyby MCP mel driv vlastnich
600 znaku a prave u nej byla cela odpoved potreba nejvic.
Cteni tela odpovedi jde pres `readBodyLimited` v `src/net/guard.ts`: cte
**proudem a usekne se u limitu**. Prvni verze nacetla cele telo a teprve pak
ho zmerila, takze `SCRIPT_MAX_RESPONSE_BYTES` chranil zaznam behu, ale ne
pamet procesu.
## Bezpecnostni hranice a co jeste chybi
Skripty ve slozce jsou **nase**, prosly gitem a code review. Bezi proto v procesu
@@ -241,6 +269,21 @@ u zakaznickych ne - ti musi bezet v izolovanem enginu ve vlastnim vlakne.
Podrobnosti v [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md), sekce
o skriptech.
### Vlastni skripty firmy bezi jinde
Skript, ktery si napise spravce firmy v portalu (krok "Vlastni skript"), neni
tenhle druh skriptu. Je to **cisty prevod hodnot** bez site a bezi
v `node:vm` (`src/runtime/sandbox.ts`) s casovym limitem. Od zari 2026 se
`utils` i `input` stavi **uvnitr kontextu vm**, ne v hostiteli: funkce
hostitele predane dovnitr nesly s sebou svuj `constructor` a pres
`constructor('return process')` se z nich dalo dostat ven. Zkompilovane
skripty se drzi v LRU cache (100) podle otisku kodu, aby se stejny skript
nekompiloval pri kazdem behu.
Porad plati, co je v komentari toho souboru: `node:vm` je izolace proti
nehode, ne proti utocnikovi. Skript pise spravce firmy, ktery jeji data uz
vidi.
## Napojeni do katalogu
Skript se domeri do katalogu sluzeb jako akce s `implementation: 'script'`
@@ -330,9 +373,8 @@ Katalog, builder i stranka skriptu si ho vezmou samy. Nic se nerestartuje.
| Chybi | Poznamka |
| ---------------------- | ---------------------------------------------------------- |
| Skripty od zakazniku | potrebuji sandbox a vlastni vlakno, viz vyse |
| Skripty od zakazniku s pristupem ven | dnes jen prevod hodnot ve `vm`, volani ven chce vlastni proces |
| Verzovani skriptu | uprava prepise soubor, historie je jen v gitu |
| Vykonavani ze stromu | runner je hotovy, ale runtime automatizaci neni |
| Skripty jako spoustece | zatim jen akce, spoustec potrebuje runtime |
| Skripty jako spoustece | zatim jen akce |
| Metriky pro widgety | manifest to zatim nezna, viz bod 4 navrhu |
| Ulozeni uprav mimo git | portal zapisuje do souboru v containeru, redeploy je vrati |
+47 -15
View File
@@ -1,21 +1,26 @@
# 14 - Databaze
Naprogramovano a overeno. Zatim se ukladaji **konektory**, tedy pristupove udaje
k sluzbam. Zbytek je v pameti procesu, poradi dalsich kroku je na konci.
Naprogramovano a overeno. Uklada se vsechno: konektory, tickety, automatizace,
incidenty, rozlozeni dashboardu, entity nastaveni, audit a notifikace.
Ukladat jde tremi zpusoby a rezim se vybira sam podle toho, co je k dispozici.
## Tri rezimy, jedno rozhrani
## Tri rezimy, jedno rozhrani, jedno rozhodnuti
Rozdil se resi **na jednom miste**, v `src/data/connectorStore.ts`. Nikde jinde
se nezjistuje, ktery rezim jede - kdyby se to rozlezlo po kodu, jedno misto by
se zapomnelo a chovalo by se pak jinak nez zbytek.
Rozdil se resi **na jednom miste**, v `initStores` v `src/data/store/index.ts`.
Nikde jinde se nezjistuje, ktery rezim jede - kdyby se to rozlezlo po kodu,
jedno misto by se zapomnelo a chovalo by se pak jinak nez zbytek.
| Rezim | Kdy | Prezije restart | Prezije redeploy |
| ---------- | --------------------------------- | --------------- | ---------------- |
| `postgres` | je `DATABASE_URL` i `SECRETS_KEY` | ano | ano |
| `file` | neni databaze, ale je `DATA_DIR` | ano | **ne** |
| `memory` | ani jedno, nebo nejde zapsat | ne | ne |
Presne to se stalo: konektory mely vlastni rozhodnuti v `connectorStore.ts`
s jinymi podminkami nez zbytek, takze konektory mohly jet z databaze a tickety
ze souboru. Ted `connectorStore` jen vola `initStores` a rezim je **jeden pro
vsechna uloziste**.
| Rezim | Kdy | Prezije restart | Prezije redeploy |
| ---------- | --------------------------------------------------------- | --------------- | ---------------- |
| `postgres` | je `DATABASE_URL`, migrace prosly a je cim sifrovat (`SECRETS_KEY`) | ano | ano |
| `file` | neni databaze, ale je `DATA_DIR` | ano | **ne** |
| `memory` | ani jedno, nebo nejde zapsat | ne | ne |
Rezim `file` je pro mockup. Filesystem containeru je docasny, takze soubor
prezije restart procesu i containeru, ale nove nasazeni ho smaze. Je to
@@ -87,8 +92,38 @@ Kdyby to byly dve implementace, jedna by se casem opravila a druha ne.
| Slucovani zapisu | deset uprav za sebou znamena jeden zapis na disk |
| Zapis pri ukonceni | `SIGTERM` dokonci rozepsany zapis, jinak by se posledni zmena ztratila |
| Rozbity soubor | prejmenuje se na `.broken`, zaloguje a jede se s prazdnymi daty. Aplikace, ktera nenastartuje, je pro AppFactory nefunkcni sluzba |
| Necitelny soubor | jen `ENOENT` je prvni start. Jina chyba cteni (prava, plny disk) **zamkne zapisy** a zaloguje se - jinak by se soubor s daty prepsal prazdnym |
| Sifrovani | tajne hodnoty jsou v souboru zasifrovane, plaintext nikdy |
Rozdil mezi poslednimi dvema radky je zamer. Rozbity JSON je zalozeny bokem
a nic se neztrati. Chyba cteni ale neznamena, ze data neexistuji - a start
s prazdnem, ktery by je pri prvnim zapisu prepsal, je jedina cesta, jak
o ne v rezimu `file` opravdu prijit.
## Vrstvy nad ulozistem
Dve obalky, kazda pro jiny druh dat (podrobne
v [17-nastaveni-a-prava.md](17-nastaveni-a-prava.md)):
**`withCache`** pro entity, ktere se ctou pri kazdem requestu a meni zridka.
Kopie v pameti, `byId` je `Map`. Vsechny cache jsou v registru
(`src/data/store/cached.ts`): `refreshCache(kind)` obnovi jednu,
`refreshAllCaches()` vsechny. Route nastaveni driv po kazdem zapisu obnovovala
vsechny cache, ted jen entitu, do ktere psala (`bootstrapDataRefresh(route)`
v `src/data/refresh.ts`). `listByTenant(tenantIds, sortBy)` je jeden filtr
a razeni misto sedmi kopii v modulech.
**`withMirror`** pro provozni data: meni se v pameti, po zmene se zapise cely
zaznam. Zapisy tehoz ID jsou **serazene za sebou** retezem promise. Bez toho
mohl Postgres potvrdit dva `put` tehoz ticketu v opacnem poradi, nez prisly,
a v tabulce zustala starsi verze - v pameti to nebylo videt, po restartu ano.
Tickety navic slucuji vic zmen v jednom tiku do jednoho zapisu
(`persist` / `flushPersist`).
**Audit** se jen pripisuje a oreza se davkou (`removeMany`) po 50 zapisech
nebo nejvys jednou za minutu. Prvni verze mazala jen radky platformy
(`tenantId: null`) a audit firem rostl donekonecna.
### Klic mimo databazi
Bez `SECRETS_KEY` si aplikace v rezimu `file` vygeneruje klic do
@@ -221,10 +256,7 @@ Proti Postgresu 16 v kontejneru:
| Chybi | Poznamka |
| ------------------------------------ | ----------------------------------------------------------------------------- |
| Automatizace v ulozisti | dalsi na rade, je to to, co si clovek nastavi. Pujde do souboru i do databaze |
| Rozlozeni dashboardu | male a samostatne, hned po automatizacich |
| Tickety a incidenty | naposled, dnes je generuje simulace |
| Uzivatele, firmy, resitele | v prototypu je to spis konfigurace nez data |
| Fronta nad Postgresem | vyber behu je v pameti jedne instance, chce to `SKIP LOCKED` |
| Sbernice udalosti pres LISTEN/NOTIFY | dnes `EventEmitter` v pameti jedne instance |
| Vymena klice (rotace) | `v` je pripravene, prevod dat napsany neni |
| Retence a partitionovani | az u tabulek behu, viz dokument 10 |
+34 -6
View File
@@ -15,7 +15,11 @@ Volající nikdy nezjišťuje, jestli běží Postgres, soubor, nebo pamět.
| Co | Kde | K čemu |
| ------------------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defineStore<T>(kind)` | `src/data/store/index.ts` | Založí úložiště pro nový druh záznamu. Jeden řádek na entitu. |
| `initStores({databaseReady})` | `src/data/store/index.ts` | Vybere režim. Volá se jednou při startu, nikde jinde. |
| `initStores({databaseReady})` | `src/data/store/index.ts` | Vybere režim pro **všechna** úložiště včetně konektorů. Volá se jednou při startu, nikde jinde. |
| `refreshCache(kind)`, `refreshAllCaches()` | `src/data/store/cached.ts` | Registr cache. Po zápisu obnovit jen tu entitu, ne všechny. |
| `listByTenant(tenantIds, sortBy)` | `src/data/store/cached.ts` | Filtr na firmu plus řazení nad cache. Místo sedmi kopií `filter` + `sort` v modulech entit. |
| `nowIso`, `minutesAgo`, `highestNumber`, `writableOrWarn` | `src/data/store/types.ts` | Drobnosti pro moduly úložišť: časová značka, čas před N minutami, nejvyšší číslo ID pro čítač, varování při zápisu do úložiště jen pro čtení. |
| `mergeValues(current, patch)` | `src/data/connectors/types.ts` | Sloučení hodnot konektoru při `PATCH`: prázdný řetězec maže, chybějící klíč nechává. Jedna implementace pro soubor i Postgres. |
| `flushStores()` | `src/data/store/index.ts` | Dopíše rozepsané zápisy. Jen při ukončení procesu. |
| `withCache(store)` | `src/data/store/cached.ts` | Kopie v paměti pro **konfigurační** entity, které se čtou při každém requestu (uživatelé, role, firmy). Čte se synchronně, obnovuje se po zápisu. |
| `withMirror(store)` | `src/data/store/mirror.ts` | Opačný směr než `withCache`: data se mění v paměti a po každé změně se celý záznam zapíše. Pro **provozní** data (tickety, automatizace, incidenty, rozložení). |
@@ -38,7 +42,17 @@ Volající nikdy nezjišťuje, jestli běží Postgres, soubor, nebo pamět.
| `navFor(...)` | `src/data/tenantFeatures.ts` | Průnik toho, co firma má, a toho, na co má člověk právo. Navigace chodí ze serveru. |
| `recordAudit(input)` | `src/data/audit.ts` | Zápis do auditu. Nevrací chybu a nečeká se - rozbitý audit nesmí rozbít aplikaci. |
| `enqueue(input)` | `src/runtime/queue.ts` | Zařadí běh. Klíč proti dvojímu zařazení drží jeden běh na jednu událost. |
| `claimBatch(limit)` | `src/runtime/queue.ts` | Vezme další práci, spravedlivě po firmách. Místo, kde nad Postgresem musí být SKIP LOCKED. |
| `claimBatch(limit, active)` | `src/runtime/queue.ts` | Vezme další práci, spravedlivě po firmách, přeskočí to, co už běží. Místo, kde nad Postgresem musí být SKIP LOCKED. |
| `touchClaim(item)` | `src/runtime/queue.ts` | Tlukot běžícího běhu. Bez něj se dlouhý běh po 30 minutách považuje za zaseknutý a vykoná se podruhé. |
| `findOpenIncident(source, tenantIds)` | `src/data/incidentStore.ts` | Otevřený incident téže příčiny v téže firmě. Stejná chyba nezakládá druhý. |
| `attachAccess`, `tenantOrDeny`, `optionalTenantOrDeny`, `scopeOrDeny` | `src/middleware/tenant.ts` | Přístup jednou na request do `req.access`, firma requestu z něj. Route si to nepočítá sama. |
| `safeRouter()`, `wrap(handler)` | `src/middleware/asyncHandler.ts` | Router, ve kterém odmítnutá promise skončí jako 500 s logem. Každá nová route vzniká tady. |
| `rateLimit({name, windowMs, max})` | `src/middleware/rateLimit.ts` | Limit requestů v paměti, 429 s `Retry-After`. Jen na veřejných endpointech, kde se dá hádat. |
| `validationError(res, zodError, message?)` | `src/middleware/validation.ts` | Jeden tvar chyby validace: z chyby zodu udělá `issues` po polích. |
| `timingSafeEqualString(a, b)` | `src/lib/secure.ts` | Porovnání tokenu v konstantním čase. Webhook, příjem, pozvánky. |
| `assertAllowedUrl`, `readBodyLimited`, `readJsonLimited`, `describeFetchError` | `src/net/guard.ts` | Vše, co volá ven: zákaz vnitřní sítě, čtení těla proudem s limitem, čitelný popis chyby sítě. HTTP skriptů, MCP, SMTP i ARES. |
| `publishOutputs(...)` | `src/runtime/executor.ts` | Zápis výstupů kroku do kontextu, jednou pro vestavěné kroky i skripty. Holé jméno nepřepíše, co už v kontextu je. |
| `lookupCompany`, `searchCompanies`, `listCompanyPersons`, `placeholderEmail`, `isPlaceholderEmail` | `src/ares/client.ts` | Registr ARES: firma podle IČ nebo názvu, statutáři, zástupná adresa a její rozpoznání. |
| `onTicketEvent(kind, ticket)` | `src/runtime/triggers.ts` | Změna ticketu zařadí navázané automatizace, včetně ochrany proti smyčce. |
| `withRun(marker, work)` | `src/runtime/context.ts` | Označí, který běh práci způsobil. Bez toho automatizace spouští sama sebe. |
| `findBuiltinStep(...)` | `src/runtime/builtinSteps.ts` | Kroky, které sahají do našeho úložiště, ne ven přes HTTP. |
@@ -50,7 +64,7 @@ Volající nikdy nezjišťuje, jestli běží Postgres, soubor, nebo pamět.
| `getAgentStats(...)` | `src/data/ticketStore.ts` | Výkon řešitelů: odbavené, mediány časů, vrácené, fronta. Používá to widget i detail osoby, aby čísla seděla. |
| `findByExternalId(...)` | `src/data/ticketStore.ts` | Ticket firmy podle externího ID. Klíč je dvojice firma a ID. |
| `findByIntakeToken(token)` | `src/data/tenants.ts` | Firma podle tokenu příjmu. Určuje i to, v jakém rozsahu je externí ID unikátní. |
| `refreshCaches()` | `src/data/bootstrap.ts` | Obnoví všechny kopie v paměti. Volá se po zápisu, který je může změnit. |
| `refreshCaches()`, `refreshEntity(kind)` | `src/data/bootstrap.ts` | Obnoví všechny kopie v paměti, nebo jen jednu entitu. Route nastavení volá `bootstrapDataRefresh(route)` v `src/data/refresh.ts`, která vybere tu jednu. |
| `bootstrapData({databaseReady})` | `src/data/bootstrap.ts` | Seznam všech entit a provozních dat. **Nová entita se přidává tady**, ne rozesetě po modulech. |
## Skripty a konektory (server)
@@ -61,8 +75,11 @@ Viz [11-skripty-konektoru.md](11-skripty-konektoru.md).
| ------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `runScript(id, inputs, ctx)` | `src/scripts/runner.ts` | Spustí skript. **Nikdy nevyhodí výjimku**, chybu vrací jako výsledek s celým hlášením. |
| `validateValues(...)` | `src/scripts/values.ts` | Jedna kontrola pro vstupy i výstupy skriptu podle manifestu. |
| `scriptUtil` | `src/scripts/util.ts` | Nádobíčko pro skripty: `pick`, `first`, `num`, `date`, `need`, `get`, `applyRules`, `fillJson`. Skript nemá sahat na nic jiného. |
| `createRedactor(...)` | `src/scripts/util.ts` | Vyškrtá tajemství z textu **před** logováním. Používá se u všeho, co jde do logu. |
| `scriptUtil` | `src/scripts/util.ts` | Nádobíčko pro skripty: `pick`, `first`, `num`, `date`, `day`, `list`, `addresses`, `quote`, `need`, `get`, `applyRules`, `fillJson`. Skript nemá sahat na nic jiného. |
| `pick`, `pickText`, `jwtExpiry`, `parseBool`, `parseNumber` | `src/scripts/util.ts` | Totéž pro server: pole bez ohledu na velikost písmen, `exp` z JWT, převody. Než napíšeš `Number(x)` s kontrolou `NaN`, je to tady. |
| `DETAIL_BYTES`, `truncate(value)` | `src/scripts/util.ts` | Jeden limit na zkracování detailu chyby pro všechny vrstvy. Žádné vlastní `slice(0, 600)`. |
| `createRedactor(...)` | `src/scripts/util.ts` | Vyškrtá tajemství z textu **před** logováním, i v URL-encoded a JSON-escaped tvaru. Používá se u všeho, co jde do logu. |
| `scriptConfig(target)` | `src/scripts/connections.ts` | Nastavení napojení bez tajných polí. Jediné, co skript dostane jako `ctx.config`. |
| `applyRules`, `fillJson` | `src/scripts/mapping.ts` | Transformace dat: pole na pole s převody, nebo objekt na objekt. Viz [13-transformace-dat.md](13-transformace-dat.md). |
| `getPath(obj, path)` | `src/scripts/mapping.ts` | Čtení `zakaznik.adresa.mesto` z neznámého objektu. |
| `resolveTarget(...)` | `src/scripts/connections.ts` | Z konektoru poskládá adresu a hlavičky. Přístupové údaje nikam jinam nevedou. |
@@ -92,7 +109,18 @@ Viz [11-skripty-konektoru.md](11-skripty-konektoru.md).
| `MappingEditor` | `components/dashboard/flow/MappingEditor.tsx` | Editor transformací v obou režimech (pole na pole, JSON). |
| `DataState` | `components/dashboard/DataState.tsx` | Načítání, chyba, prázdno. Ať to každá stránka nekreslí po svém. |
| `apiFetch<T>` | `lib/api.ts` | Jediná cesta na API: base path, token, `ApiError` s celým hlášením ze serveru. |
| `useApiQuery<T>` | `lib/useApiQuery.ts` | Načtení dat do stránky včetně `reload`. S `body` pošle POST (dávkové načtení), s `enabled: false` se neptá vůbec. |
| `useApiQuery<T>` | `lib/useApiQuery.ts` | Načtení dat do stránky včetně `reload`, `refreshing` a `total`. S `body` pošle POST, s `enabled: false` se neptá, `patchOn` opraví data z události bez dotazu. |
| `useCollection(key)`, `useAccess()`, `useCollectionSelector` | `lib/collections.tsx` | Číselníky za firmu (lidé, skupiny, typy, služby, konektory, přístup) ze sdíleného skladu, opravované z událostí. Ne `apiFetch` na číselník ze stránky. |
| `patchTicketList(...)` | `lib/ticketEvents.ts` | Oprava seznamu ticketů z `payload.ticket` v události. Použít jako `patchOn`. |
| `apiFetchWithMeta<T>` | `lib/api.ts` | Jako `apiFetch`, ale vrací i `X-Total-Count`. Pro stránkované seznamy. |
| `useSubmit(fn)` | `lib/useSubmit.ts` | Odeslání formuláře: `saving`, chyba, reset. Dvanáct řádků, které si dřív psal každý formulář zvlášť. |
| `useUnsavedChanges(dirty)` | `lib/useUnsavedChanges.ts` | Varování při odchodu z rozepsaného formuláře nebo stromu. |
| `priorities`, `priorityLabel` | `lib/options.ts` | Pevné číselníky. Stavy a kanály se berou ze serveru (`/widget-data/options`). |
| `plural(count, forms)` | `lib/format.ts` | Skloňování počtu (1 ticket, 2 tickety, 5 ticketů). |
| `Field`, `Input`, `Select`, `Textarea` | `components/ui/form/` | Formulářové prvky s jednou sadou tříd (`controlClass`). Vlastní `inputClass` ve stránce je chyba. |
| `Chip` | `components/ui/Chip.tsx` | Štítek. |
| `TicketCard` | `components/dashboard/TicketCard.tsx` | Karta ticketu pro dlaždice a mobil, varianta `compact` pro widgety. |
| `useMediaQuery(query)` | `lib/useMediaQuery.ts` | Tabulka nebo karty podle šířky. `TicketTable` podle toho kreslí obojí, druhá komponenta není. |
| `cn(...)` | `lib/cn.ts` | Skládání tříd. Podmíněné třídy nikdy ručně přes šablonu. |
| `format*` | `lib/format.ts` | Čísla, procenta, datum, relativní čas, trvání. Formátování se nepíše v komponentě. |
| `serviceIcon(key)` | `lib/serviceIcons.ts` | Klíč ikony ze serveru na komponentu. Server neposílá komponenty. |
+38 -3
View File
@@ -24,6 +24,11 @@ Nová entita v nastavení pak znamená: `defineStore` v modulu entity, jeden ř
v `bootstrap.ts`, jeden `crudRouter` v `settings.ts`, jeden popis v
`Settings.tsx`. Nic víc.
`crudRouter` navic s volbou `event` publikuje `<druh>.created`, `.updated`
a `.deleted` s celym zaznamem v payloadu, takze klientsky sklad ciselniku
(`lib/collections.tsx`) se opravi bez dotazu. Po zapisu se obnovi **jen cache
te entity** (`bootstrapDataRefresh(route)`), ne vsechny.
## Práva jsou data
Práv je 26 a jsou v katalogu (`src/data/permissions.ts`). Role je **záznam**,
@@ -37,6 +42,36 @@ v Automii řešitel a u Nordisu správce jejich servicedesku.
Neznámé právo se **odmítne** už při ukládání role. Kdyby se jen ignorovalo,
překlep by znamenal roli, která tiše nic nesmí.
### Prava se kontroluji u kazde route, za firmu zaznamu
Do zari 2026 se vetsina rout ptala jen "je clen firmy". Prava v katalogu
byla, ale `viewer` mohl zalozit konektor nebo smazat automatizaci. Ted:
| Co | Pravo |
| ---------------------------------------- | -------------------------------------------------------- |
| firmy | jen spravce platformy |
| uzivatele | spravce platformy, nebo `user.manage` jen ve sve firme |
| pozvanky | `user.manage`, role jen z te firmy |
| konektory (zalozeni, uprava, smazani, test) | `connector.manage` |
| automatizace (zalozeni, uprava, smazani, novy token) | `automation.edit` |
| vestavene akce na ticketu | pravo akce za firmu ticketu plus strop viditelnosti |
| prepnuti na jiny ucet, audit | `impersonate`, `audit.view` |
Spravce firmy s `user.manage` nenastavi `platformAdmin`, neprida clenstvi
v jine firme, nesahne na spravce platformy a nesmaze cloveka, ktery je i
v jine firme. Duvody a rozhodnuti "kdo koho zaklada" jsou
v [07-firmy-a-prava.md](07-firmy-a-prava.md).
### Firma z registru ARES
Zalozka Firmy ma vedle rucniho zalozeni cestu pres ARES
(`components/dashboard/AresTenantDialog.tsx`), jen pro spravce
platformy: IC nebo nazev, vyber firmy, vyber statutaru, kteri dostanou ucet
s roli spravce a zastupnou adresou `IC-poradi@placeholder.cz`. Zastupne
adresy se musi nahradit skutecnymi, jinak se ti lide neprihlasi. Firma pak
nese `ico`, `dic`, `address` a `legalForm`, IC je unikatni. Endpointy jsou
v [04-api.md](04-api.md).
## Navigace chodí ze serveru
Co uživatel vidí za záložky, je průnik dvou věcí:
@@ -119,9 +154,9 @@ takže ho nemůže ani omylem prodloužit.
| Data | Jak | Proč |
| ----------------------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------- |
| Firmy, uživatelé, role, řešitelé, skupiny, typy, akce, widgety, záložky | `withCache` | Čtou se při každém requestu, mění se zřídka. Kopie v paměti, obnova po zápisu. |
| Tickety včetně logu, automatizace, incidenty, rozložení dashboardu | `withMirror` | Mění se v paměti za provozu, po každé změně se celý záznam zapíše. |
| Audit | přímo do úložiště | Jen se připisuje, nikdy nečte při každém requestu. |
| Firmy, uživatelé, role, řešitelé, skupiny, typy, akce, widgety, záložky | `withCache` | Čtou se při každém requestu, mění se zřídka. Kopie v paměti (`byId` je `Map`), obnova jen te entity po zápisu. |
| Tickety včetně logu, automatizace, incidenty, rozložení dashboardu | `withMirror` | Mění se v paměti za provozu, po každé změně se celý záznam zapíše. Zápisy téhož ID jdou za sebou, ne naráz. |
| Audit | přímo do úložiště | Jen se připisuje, nikdy nečte při každém requestu. Oreza se davkou po 50 zapisech nebo jednou za minutu. |
| Konektory | vlastní úložiště | Nesou šifrovaná tajemství a potřebují částečný unikátní index. Viz [14-databaze.md](14-databaze.md). |
Čítače ID se při startu dopočítají z uložených záznamů, takže nový ticket
+69 -8
View File
@@ -84,7 +84,23 @@ odmítnout ho kvůli tomu by znamenalo, že webhook nejde zapojit.
těla. V portálu je u adresy vidět totéž včetně metody, a kopíruje se **celá
adresa včetně domény**.
## Opakování a vzdání se
## Worker je pool
`CONCURRENCY` (4) behu naraz, ale **nezavisle na sobe**. Worker drzi mnozinu
`active`, kazde kolo si vezme `CONCURRENCY - active.size` behu
(`claimBatch(limit, active)` to, co uz bezi, preskoci) a spusti je bez cekani
na ostatni. Prvni verze brala davku a cekala, az dobehne cela: jeden pomaly
krok MCP na deset minut blokoval tri prazdne sloty.
Bezici beh posila kazdou minutu **tlukot** (`touchClaim`). Za zaseknuty se
povazuje az 30 minut od posledniho tlukotu (`STUCK_AFTER_MS` v `queue.ts`),
ne od vzeti z fronty. Driv to bylo deset minut od vzeti, takze beh s dlouhou
ulohou MCP se vratil do fronty a **vykonal se podruhe**, i kdyz porad bezel.
Planovac zarazuje behy s triggerem `poll`, ne `manual`, aby slo v seznamu
behu poznat, co spustil clovek a co cas.
## Opakovani a vzdani se
| Pokus | Kdy |
| ----- | --------- |
@@ -94,12 +110,52 @@ adresa včetně domény**.
| 4. | za 10 min |
| 5. | za hodinu |
Pak běh skončí jako `failed` a zůstane k nahlédnutí. Nemaže se: bez záznamu
by nikdo nezjistil, že se něco nestalo.
Pak beh skonci jako `failed` a zustane k nahlednuti. Nemaze se: bez zaznamu
by nikdo nezjistil, ze se neco nestalo.
**Marná chyba se neopakuje vůbec.** Chybějící skript nebo neexistující skupina
za minutu existovat nezačne, takže se běh rovnou vzdá. Opakuje se jen to, co
může pominout: nedostupná služba, timeout.
**Opakuje se jen to, co samo rekne `retryable`.** Vychozi je "ne". Pravidlo
je stejne ve vsech vrstvach a je napsane v komentari nad `StepResult`
v `executor.ts`:
| Opakuje se | Konci hned a zaklada incident |
| ------------------------------------------------------- | ---------------------------------------------------------- |
| chyba spojeni, timeout pred odeslanim | 401, 403, 404 |
| 5xx a 429 od cizi sluzby | spatny vstup, chybejici vystup, `ctx.fail` |
| vypadek uloziste konektoru | chybejici nebo pozastavena automatizace, chybejici skupina |
| MCP: chyba spojeni, 408, 425, 429, 502, 503, 504 | MCP: timeout uz odeslaneho `tools/call`, chyba v kodu |
Driv se opakovala **kazda** chyba skriptu, petkrat za 72 minut. Spatny vstup
tak petkrat zopakoval tutez hlasku a u kroku, ktery neni idempotentni, mohl
cizi sluzbu zavolat podruhe. Timeout uz odeslaneho volani MCP je proto
neopakovatelny: MCP nema idempotencni klic a jestli druhy pokus znamena druhou
objednavku, vi jen server, ktery neni nas.
## Dva stropy na velikost behu
| Strop | Co pocita |
| -------------------- | --------------------------------------------------- |
| `MAX_STEPS = 50` | kroky ve stromu, kontroluje se pri ulozeni |
| `MAX_ACTIONS = 1000` | vykonane kroky vcetne pruchodu smyckou, za behu |
Jeden strop nestacil: smycka se dvema kroky nad 26 polozkami je 52 vykonanych
kroku a beh padal na limitu 50, i kdyz strom mel kroky ctyri. Kdyz se strop
vycerpa, beh se zastavi a rekne to; tise useknout smycku by vypadalo jako
uspech.
## Podminka nad datem
Pole typu `date` a hodnoty, ktere vypadaji jako ISO datum, se v podmince
porovnavaji pres `Date.parse`. Driv slo vsechno pres `Number()`, ISO retezec
vysel jako `NaN` a `gt` i `lt` nad datem byly **vzdycky nesplnene**, bez chyby
a bez radku v logu.
## Vystupy kroku
`publishOutputs()` v `executor.ts` je jedno misto pro vestavene kroky
i skripty. Vystup se zapise pod jmenem kroku (`st_x.status`) vzdycky, **hole
jmeno (`status`) jen kdyz v kontextu jeste neni**. Nastroj MCP, ktery vraci
`status`, driv prepsal `status` spoustece a podminka za nim se ptala na
spatnou hodnotu.
## Incident z chyby
@@ -118,6 +174,12 @@ informace pro zákazníka.
Stejná příčina nezakládá druhý incident, dokud je první otevřený. Jinak by
deset stejných chyb znamenalo deset incidentů a nikdo by se v tom nevyznal.
Incident patri **firme behu**. `findOpenIncident(source, tenantIds)` hleda jen
v ni a krok `incident/create` predava `tenantId` - prvni verze zakladala
globalni incident, ktery videly vsechny firmy. Neocekavana vyjimka v kroku
(chyba v kodu) je taky koncova: neopakuje se a zaklada incident, protoze za
minutu nezmizi.
## Ochrana proti smyčce
Automatizace navázaná na změnu ticketu ticket změní, čímž se spustí znovu.
@@ -205,8 +267,7 @@ Ověřeno 19 kontrolami proti běžícímu serveru.
to musí být `SELECT ... FOR UPDATE SKIP LOCKED`, jinak si dva workery
vezmou tentýž běh. Místo je označené v `runtime/queue.ts`.
- **Strop souběžných volání na dvojici firma a služba** a vypnutí služby po
sérii chyb. Timeout a rozlišení "zkusit znovu / marné" už ve
`scripts/http.ts` je.
sérii chyb. Rozliseni "zkusit znovu / marne" uz plati ve vsech vrstvach.
- **Dlouhé čekání** (pošli e-mail za tři dny) přes běh naplánovaný na později.
Krok `flow/pause` umí nejvýš minutu, protože blokuje běh.
+14 -14
View File
@@ -1,6 +1,7 @@
# 23 - Jazyky
Rozdelane. Mechanismus je hotovy a overeny, prevod obsahu bezi po castech.
Mechanismus je hotovy a overeny. Verejny web je prelozeny cely, portal za
prihlasenim jen ve spolecnych castech.
## Jak to funguje
@@ -51,19 +52,18 @@ v nadpisu.
## Co uz je prelozene
| Cast | Stav |
| ----------------------------------- | ------------------------------------ |
| Hlavicka a mobilni menu | hotovo |
| Hero vcetne snimku portalu | hotovo |
| Pas s logy klientu | hotovo |
| Cisla | hotovo |
| Zaverecna vyzva | hotovo |
| Paticka | castecne, sloupce odkazu zatim cesky |
| Sekce Produkty, Reference, Postup | ne |
| Stranky O nas, Sluzby, Kontakt, 404 | ne |
| Portal za prihlasenim | ne |
| Cast | Stav |
| ----------------------------------------- | --------------------------------------------- |
| Hlavicka, navigace a mobilni menu | hotovo |
| Homepage vcetne Produktu, Referenci a Postupu | hotovo |
| Paticka | hotovo |
| Stranky O nas, Sluzby, Kontakt, 404 | hotovo |
| Prihlaseni | hotovo, vcetne hlasky o vyprselem prihlaseni |
| `DataState` a `ErrorBoundary` | hotovo - stavy nacitani a chyby jsou vsude |
| Portal za prihlasenim | ne, stranky dashboardu maji texty v kodu |
Prevod zbytku je mechanicky: text ven do `cs.ts`, klic do `en.ts`, v komponente
Pro prelozene casti je `en.ts` **uplna**, nikde nespada na cestinu. Zbytek je
mechanicky: text ven do `cs.ts`, klic do `en.ts`, v komponente
`const t = useT()` a `{t('klic')}`. Zadna dalsi prace na vrstve uz potreba neni.
## Co se neprekada
@@ -78,7 +78,7 @@ Claim se **prekada** (`brand.claim`), protoze to je text, ne udaj.
| Chybi | Poznamka |
| ---------------------- | ----------------------------------------------------------------- |
| Prevod zbytku obsahu | viz tabulka vys |
| Stranky portalu | viz tabulka vys |
| Jazyk v adrese | `/en/sluzby` misto volby v prohlizeci. Chce to kvuli vyhledavacum |
| Preklad dat ze serveru | nazvy sluzeb, stavu a chybovych hlasek chodi z API cesky |
| Format cisel a datumu | `toLocaleString` se zatim vola natvrdo s `cs-CZ` |
+44 -4
View File
@@ -202,6 +202,41 @@ vedet, jak s nimi portal nalozil, nez zacne hledat chybu jinde.
token**, ne pred kazdym volanim. Server drzi sezeni u tokenu, takze po jeho
vymene se to musi zopakovat, jinak odpovi, ze relace neni inicializovana.
Plati to i pro server **bez prihlaseni** - i ten ma sezeni a handshake stoji
dve volani. Prvni verze cachovala jen podle tokenu, takze server bez tokenu
dostaval handshake pred kazdym krokem.
`Mcp-Session-Id`, ktery server vyda pri handshaku, se **uklada s handshakem**
a posila v kazdem dalsim volani (u dialektu, ktere hlavicku pouzivaji).
Sezeni bez ID by server nepoznal a kazde volani by zacinalo znovu. Kdyz
server na preskoceny handshake odpovi 400 nebo 404, znamena to podle
specifikace "sezeni neznam": handshake se zopakuje **jednou** a volani se
posle znovu. Vic nez jednou ne - kdyby to nepomohlo, je chyba jinde a smycka
by ji jen schovala.
Prihlaseni OAuth u obecne sluzby sdili **jednu rozdelanou operaci na
konektor**, stejne jako EasyWeb: dva behy nad tymz napojenim udelaji jedno
prihlaseni, ne dve.
## Co se opakuje a co ne
| Situace | Opakuje se |
| ----------------------------------------------- | ---------- |
| spojeni se nenavazalo, DNS, TLS | ano |
| 408, 425, 429, 502, 503, 504 | ano |
| timeout **uz odeslaneho** `tools/call` | **ne** |
| 401, 403, 404, chyba prihlaseni | ne |
| `isError` v odpovedi nastroje | ne |
Timeout po odeslani je ta zvlastni radka. MCP nema idempotencni klic, takze
druhy pokus by nastroj provedl podruhe - a jestli to znamena druhou
objednavku, vi jen server, ktery neni nas. Rozhoduje `McpFailure.retryable`,
stejne pravidlo jako v [20-fronta-a-runtime.md](20-fronta-a-runtime.md).
Chyby prihlaseni maji spolecneho predka `AuthFailure` v `src/mcp/errors.ts`,
`EasyWebAuthError` z nej dedi. Krok tak pozna chybu prihlaseni jednou
kontrolou a detail je vzdy zredigovany, at prisel odkudkoliv.
## Odpoved muze byt stream
Server si sam vybira, jestli odpovi JSON telem, nebo SSE streamem, a **streamem
@@ -348,11 +383,14 @@ a krok to rekne misto toho, aby predstiral selhani.
ostatni tajne hodnoty.
- **Cizi napojeni se chova jako neexistujici.** Krok si konektor nacita pres
filtr na firmu, takze strom s cizim ID konektoru selze.
- **Krok se neopakuje.** MCP nema idempotencni klic, takze druhy pokus po
timeoutu by nastroj provedl podruhe - a jestli to znamena druhou objednavku,
vi jen server, ktery neni nas.
- **Odeslane volani se neopakuje.** MCP nema idempotencni klic, takze druhy
pokus po timeoutu by nastroj provedl podruhe. Opakuje se jen to, co selhalo
pred odeslanim nebo co server odmitl docasne, viz vyse.
- Plati stejny strop na velikost odpovedi jako u skriptu
(`SCRIPT_MAX_RESPONSE_BYTES`), a to i u streamu, kde se pocita prubezne.
Cte se pres `readBodyLimited` z `src/net/guard.ts`, stejne jako u HTTP
skriptu a SMTP. Detail chyby ma stejny limit jako vsechno ostatni
(`DETAIL_BYTES`), driv mel vlastnich 600 znaku.
- Seznamy se strankuji nejvys stokrat, volani nastroje dvacetkrat.
## Co se **nedela**
@@ -373,6 +411,8 @@ a krok to rekne misto toho, aby predstiral selhani.
| Rozdily serveru | `src/mcp/dialect.ts` |
| Protokol | `src/mcp/client.ts` |
| Prihlaseni obecne | `src/mcp/auth.ts` |
| Chyby prihlaseni | `src/mcp/errors.ts` |
| Sit a limity tela | `src/net/guard.ts` |
| Klice EasyWebu | `src/mcp/easyweb/crypto.ts` |
| Zarizeni u konektoru | `src/mcp/easyweb/device.ts` |
| Tokeny EasyWebu | `src/mcp/easyweb/session.ts` |
@@ -382,7 +422,7 @@ a krok to rekne misto toho, aby predstiral selhani.
| Nacteni nastroju | `src/routes/connectors.ts` |
| Vykonna cast kroku | `src/runtime/builtinSteps.ts`, `runMcpTool` |
| Ulozeni u konektoru | `src/data/connectors/*`, migrace `004` |
| Portal | `web/src/pages/dashboard/Connectors.tsx` |
| Portal | `web/src/pages/dashboard/Connectors.tsx` a `connectors/{ConnectorCard,ConnectorEditor,ConnectorLogs,ConnectorTools}.tsx` |
## Co jeste chybi
+31 -16
View File
@@ -6,9 +6,12 @@
[07-firmy-a-prava.md](07-firmy-a-prava.md),
- sekce 6, kontrakt webhooku,
- z prvni sekce dlazdice "Moje tickety" a "Fronta bez resitele" vcetne noveho
vychoziho rozlozeni.
vychoziho rozlozeni,
- sekce 2, formularova vrstva (zari 2026, viz zacatek te sekce),
- ze sekce 3 strankovani na serveru: `/tickets` bere `limit` a `offset`
a vraci `X-Total-Count`, viz [04-api.md](04-api.md).
Zbyva widget akci, "Zaciname", formularova vrstva a hledani. Az se cast udela,
Zbyva widget akci, "Zaciname" a hledani. Az se cast udela,
prepise se do prislusneho souboru dokumentace a odsud zmizi - stejne pravidlo
jako u [09-navrh-rozsireni.md](09-navrh-rozsireni.md).
@@ -170,9 +173,23 @@ coz je cil, to sedi.
---
## 2 - Formularova vrstva
## 2 - Formularova vrstva (hotovo)
### Cim to je
**Naprogramovano v zari 2026**, presne podle navrhu nize:
| Navrh | Kde to je |
| ------------------------------- | ---------------------------------------------------------------- |
| `Field`, `Input`, `Select`, `Textarea` | `components/ui/form/`, tridy v `controlClass.ts` |
| `useSubmit` | `lib/useSubmit.ts` |
| ciselniky ven | `lib/options.ts` (priority), stavy a kanaly z `/widget-data/options` |
| kompaktni karta ticketu | `components/dashboard/TicketCard.tsx`, varianta `compact` |
| stitek | `components/ui/Chip.tsx` |
| sklonovani poctu | `plural()` v `lib/format.ts` |
Zmizelo 15 kopii trid vstupniho pole. Navrh zustava nize jako zduvodneni,
proc to vypada tak, jak vypada.
### Cim to bylo
V `components/ui/` je Badge, Button, Card, Container, Modal, PageHeader, Section
a Spinner. **Zadny formularovy prvek.** Takze si ho kazda stranka pise znovu.
@@ -278,12 +295,14 @@ Klientsky filtr pres ctyri veci: `id`, `subject`, `customer.company`,
polich**, ve **stitcich** ani v **externim ID**. To posledni je zrovna to, cim se
dohledava hovor: clovek ma `CAbc75a8...` a chce ten ticket. Dnes ho nenajde.
A funguje to jen proto, ze `GET /api/dashboard/tickets` vraci **vsechny tickety
firmy najednou**, bez limitu a bez strankovani. Pri par desitkach to nevadi, pri
deseti tisicich jsou to megabajty do prohlizece pri kazdem otevreni seznamu.
A funguje to jen proto, ze seznam v portalu si bere **vsechny tickety firmy
najednou**. Server uz strankovani umi (`limit`, `offset`, `X-Total-Count`),
klient ho zatim nepouziva. Pri par desitkach to nevadi, pri deseti tisicich
jsou to megabajty do prohlizece pri kazdem otevreni seznamu.
Hledani pres modal proto znamena **serverovy endpoint**, a je to zaroven
prilezitost prestat posilat vsechno.
chvile, kdy seznam prejde na strankovani - jinak by klientske hledani
a serverove ukazovaly jina cisla.
### Tvar
@@ -576,14 +595,10 @@ prestane stacit.
## Poradi praci
1. **Formularove primitivy** a rozdeleni dvou formularu (ticket, helpdesk). Nic
dalsiho na nich nestoji, ale stoji na nich vsechno ostatni v rozhrani.
2. **"Moje tickety" a "Fronta bez resitele".** Formulare nepotrebuji, jsou skoro
zadarmo a jsou hned videt v provozu.
3. **Viditelnost.** Model, strop v `resolveScope`, vynuceni v jedne ceste ke
ticketum, panel skupiny. Delat to pred hledanim, ne po nem, jinak se hledani
pise dvakrat.
4. **Modal hledani** a serverovy endpoint se strankovanim.
1. **Formularove primitivy** - hotovo.
2. **"Moje tickety" a "Fronta bez resitele"** - hotovo.
3. **Viditelnost** - hotovo, viz [07-firmy-a-prava.md](07-firmy-a-prava.md).
4. **Modal hledani** a prechod seznamu na strankovani, ktere server uz umi.
5. **Widget akci** a "Zaciname". Sahne uz jen na hotove formulare.
Postgres kdykoliv mezi tim, nezavisle na ostatnim.
+246
View File
@@ -2,6 +2,252 @@
Nejnovejsi nahore.
## 2026-09-09 - Revize projektu: prava, vykon, runtime, portal a ARES
Velka sada oprav napric celym projektem. Zadna nova obrazovka, ale skoro kazda
vrstva se zmenila v tom, **co dela pri zatezi a pri chybe**. K tomu jedna nova
funkce: zalozeni firmy podle registru ARES. Zaznam je dlouhy schvalne - tohle
je misto, kde se za pul roku hleda, proc se neco chova tak, jak se chova.
### Uloziste: jeden rezim pro vsechno
Rozhodnuti o rezimu (`postgres`, `file`, `memory`) delal `connectorStore.ts`
pro konektory a `initStores` pro zbytek, kazdy podle svych podminek. Mohlo se
stat, ze konektory jely z databaze a tickety ze souboru. Ted rozhoduje
**jen `initStores` v `src/data/store/index.ts`**: Postgres jen kdyz je
`DATABASE_URL`, migrace prosly a je cim sifrovat (`SECRETS_KEY`), jinak soubor
nebo pamet pro vsechna uloziste vcetne konektoru. `connectorStore` uz jen vola
`initStores`.
Dalsi opravy v ulozisti, kazda ma za sebou konkretni problem:
| Co | Proc |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| audit se maze davkou (`removeMany`) | orezavani mazalo jen radky platformy, audit firem rostl donekonecna. Bezi po 50 zapisech nebo nejvys jednou za minutu |
| `markRead` pres `updateMany` | notifikace delaly jeden zapis a jednu obnovu cache na kazdy zaznam |
| `persist` / `flushPersist` u ticketu | vic zmen tehoz ticketu v jednom tiku je jeden zapis, ne pet |
| `withMirror` radi zapisy za sebou | dva `put` tehoz zaznamu mohl Postgres potvrdit v opacnem poradi a v tabulce zustala starsi verze. Ted je na kazde ID retez promise |
| `issues` a `stepCount` ulozene | `listAutomations` validoval vsechny stromy pri kazdem cteni. Ted se spocitaji pri ulozeni (`withDerived`) a jednou pri startu |
| registr cache (`refreshCache(kind)`) | route nastaveni obnovovala vsechny cache, ted jen tu entitu, do ktere psala (`bootstrapDataRefresh(route)`) |
| `listByTenant(tenantIds, sortBy)` | sedm kopii filtr + razeni v modulech entit |
| mapy misto `find` | `withCache.byId` je `Map`, ticketStore ma `ticketsById` a `ticketsByExternal`, vytizeni a statistiky se seskupi jednim pruchodem |
| `create` v lokalnim ulozisti hazi na duplicitu | Postgres to delal, soubor tise prepsal |
| `snapshot.ts` prepise soubor jen pri ENOENT | jina chyba cteni (prava, plny disk) driv znamenala start s prazdnymi daty a **prepsani souboru prazdnym obsahem**. Ted se zapisy zamknou a zaloguje se to |
| `listIncidents(tenantIds)` povinne | stejne pravidlo jako u ticketu, incident byl posledni seznam bez filtru |
Slovnik stavu ticketu je sjednoceny na cesky `defaultStatuses` (Novy, V reseni,
Ceka na klienta, Vyreseno) a ukazkove widgety filtruji `closed: false`, ne
podle nazvu stavu. Ukazkove tickety TK-4817 a TK-4812 vznikaji jen se
`SEED_DEMO=1`. V `services.ts` byla dvakrat operace `set-status`, druha tise
prekryvala prvni; `checkOperationIds()` to ted pri nacteni zaloguje.
Spolecne pomocne funkce, aby se nepsaly po modulech: `nowIso`, `minutesAgo`,
`highestNumber`, `writableOrWarn` v `store/types.ts`, `mergeValues`
v `connectors/types.ts`.
### Runtime: worker je pool a opakuje se jen to, co muze pominout
Worker bral davku ctyr behu a cekal, az dobehnou vsechny. Jeden pomaly beh
tak blokoval tri volne sloty. A beh delsi nez deset minut se povazoval za
zaseknuty, vratil se do fronty a **vykonal se podruhe**. Ted:
- `active` je mnozina bezicich ID, kazde kolo si vezme `CONCURRENCY - active.size`
behu a spusti je bez cekani na ostatni,
- `claimBatch(limit, active)` preskakuje to, co uz bezi,
- beh kazdou minutu posle tlukot (`touchClaim`) a za zaseknuty se povazuje az
30 minut od posledniho tlukotu (`STUCK_AFTER_MS`), ne od vzeti z fronty.
**Opakovani.** Kazda chyba skriptu se opakovala petkrat za 72 minut, i 403
a spatny vstup. Ted se krok opakuje jen kdyz sam rekne `retryable`: chyba
spojeni, timeout, 5xx a 429, vypadek uloziste konektoru. 401, 403, 404,
validace a `ctx.fail` konci hned a zakladaji incident. U MCP jsou opakovatelne
chyby spojeni a 408, 425, 429, 502, 503, 504; **timeout uz odeslaneho
`tools/call` opakovatelny neni**, protoze MCP nema idempotencni klic a nastroj
by se provedl podruhe. Pravidlo je v komentari nad `StepResult`
v `executor.ts`, aby ho nasel kazdy, kdo pise novy druh kroku.
Dalsi zmeny v behu:
| Co | Proc |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `MAX_ACTIONS = 1000` | `MAX_STEPS = 50` pocita staticky strom. Smycka se dvema kroky nad 26 polozkami narazila na 50 a beh spadl. Vykonane kroky maji vlastni strop |
| datum v podmince pres `Date.parse` | `gt` a `lt` nad datem prevadely ISO retezec cislem, vyslo NaN a podminka byla vzdycky nesplnena |
| `publishOutputs()` spolecne | vestavene kroky a skripty publikovaly vystupy kazdy jinak. Hole jmeno se zapise jen kdyz v kontextu jeste neni - nastroj MCP vracejici `status` prepisoval `status` spoustece |
| sandbox stavi `utils` i `input` uvnitr vm | funkce hostitele prosakovaly do skriptu firmy a `constructor('return process')` z nich utekl ven. Zkompilovane skripty se cachuji (LRU 100) |
| chybejici nebo pozastavena automatizace | neopakovatelna chyba plus incident, driv se to zkouselo dokola |
| `incident/create` nese `tenantId` | krok zakladal globalni incident, ktery videly vsechny firmy |
| planovac zarazuje s triggerem `poll` | bylo `manual`, takze se v behu nedalo poznat, ze to spustil planovac |
### Sit a tajemstvi
Novy `src/net/guard.ts` sdruzuje to, co melo kazde volani ven zvlast:
`assertAllowedUrl` (zakaz vnitrni site), `describeFetchError`,
`readBodyLimited` a `readJsonLimited`. Telo se **cte proudem a usekne se
u limitu** - driv se nacetlo cele a teprve pak zmerilo, takze limit nechranil
pamet. Pouziva to HTTP skriptu, klient MCP, prihlaseni MCP i SMTP.
Redaktor masky navic maskuje tajemstvi v **URL-encoded a JSON-escaped** tvaru,
protoze cizi sluzby je v chybach vraceji i tak. `ctx.config` skriptu uz nikdy
neobsahuje tajna pole (`scriptConfig`). `EasyWebAuthError` dedi z `AuthFailure`
(novy `src/mcp/errors.ts`), takze se chyby prihlaseni poznaji jednou
kontrolou a detail je vzdy zredigovany.
Klient MCP: handshake se cachuje i pro server bez prihlaseni, `Mcp-Session-Id`
se uklada s handshakem a posila znovu, 400 nebo 404 po preskocenem handshaku
vyvola jeden novy handshake. OAuth prihlaseni sdili rozdelanou operaci na
konektor. V `delay()` unikal posluchac abortu.
`src/scripts/util.ts` dostal `pick`, `pickText`, `jwtExpiry`, `parseBool`,
`parseNumber` a jednu konstantu `DETAIL_BYTES` na zkracovani (MCP mel 600
znaku, zbytek 8 kB). `ctx.util` skriptu ma navic `day`, `list`, `addresses`,
`quote`; sablona `scripts/_sablona.js` je vypisuje a osm skriptu je pouziva.
### API: prava se kontroluji za firmu a u kazde route
Prava byla ve vetsine rout jen "je prihlaseny" nebo "je clen firmy". Ted:
| Route | Kdo smi |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| firmy CRUD | jen spravce platformy |
| uzivatele CRUD | spravce platformy vse. Spravce firmy (`user.manage`) jen lidi sve firmy, nenastavi `platformAdmin`, neprida clenstvi jinde, nesahne na spravce platformy a nesmaze cloveka, ktery je i v jine firme |
| pozvanky | `user.manage` a jen role te firmy |
| konektory create, update, delete, test | `connector.manage` |
| automatizace create, update, delete, regenerate | `automation.edit` za firmu automatizace |
| `/services`, `/connectors/services` | clenstvi ve firme, cizi firma je 404 |
| assign, status, comment, claim | `builtinAction` v `ticketActions.ts`, pravo za firmu ticketu a strop viditelnosti (`visibleTicketOrDeny`) |
| `/api/admin/*` | `impersonate` a `audit.view` se ted opravdu kontroluji |
Nove middleware, kazde s jednim ukolem:
| Soubor | Co |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `middleware/asyncHandler.ts` | `wrap`, `safeRouter`: odmitnuta promise v handleru driv zabila proces. `unhandledRejection` se loguje, `uncaughtException` loguje a ukonci |
| `middleware/rateLimit.ts` | klouzave okno v pameti: login 20 za 15 min, kontakt 5 za hodinu, prijeti pozvanky 5 za 15 min. 429 s `Retry-After` |
| `middleware/tenant.ts` | `attachAccess` spocita pristup jednou na request do `req.access`; `tenantOrDeny`, `optionalTenantOrDeny`, `scopeOrDeny` misto kopii v routach |
| `middleware/validation.ts` | `validationError`, jeden tvar `{ error, message, issues: [{ field, message }] }` |
| `lib/secure.ts` | `timingSafeEqualString` pro tokeny webhooku, prijmu a pozvanek |
V `index.ts`: log requestu maskuje tokeny za `/webhook/`, `/webhook/ticket/`
a `/invites/`; bezpecnostni hlavicky (nosniff, `X-Frame-Options SAMEORIGIN`,
`Referrer-Policy`, `Permissions-Policy`); `trust proxy` = 1, aby limit
pocital s adresou klienta a ne proxy; rozpoznani API 404 bere
`config.rootPath` misto natvrdo `/apps/`. Dockerfile instaluje `npm ci`.
Vykon: data widgetu nactou seznam ticketu jednou na request, ne za kazdy
widget; `/tickets` a `/runs` berou `limit` a `offset` (nejvys 500) a vraceji
`X-Total-Count`, `/tickets` i `total` v tele; `hashPassword` je asynchronni,
aby bcrypt neblokoval smycku; `/people/:id` prochazi tickety jednou.
`GET /api/dashboard/access` vraci `roleNames`, aby klient nehadal popisek role.
`/storage` a `/scripts` vraceji cesty na serveru jen spravci platformy.
V `openapi.ts` pribylo 22 chybejicich cest, `/whoami` je opraveny a `features`
uz nejsou popsane jako CRUD.
### Udalosti nesou firmu
`DashboardEvent.tenantId` (null = cela platforma). Stream SSE filtruje zive
udalosti i historii podle firem uzivatele, udalost s `payload.userId` jde jen
tomu cloveku. Driv videl kazdy prihlaseny udalosti vsech firem.
Nove udalosti entit `tenant|user|role|person|group|ticketType|action|widget|connector|feature`
s `.created|.updated|.deleted`, publikuje je `crudRouter` (volba `event`),
routy konektoru a PUT features. Payload je `{ id, <druh>: zaznam }`, u smazani
`{ id }`. Udalosti ticketu `ticket.updated`, `ticket.assigned`,
`ticket.resolved` nesou v `payload.ticket` cely ticket, takze klient opravi
seznam na miste a nemusi se ptat znovu.
### Portal: obnova bez odmontovani a klientsky sklad ciselniku
Kazda udalost ze streamu odmontovala stranku: `useApiQuery` prepnul `loading`
a `DataState` vykreslil spinner misto deti. Ted je `loading` jen do prvnich
dat, potom `refreshing`, a deti zustavaji. Hooky sdili jeden debounce 150 ms,
cache modulu a deduplikaci bezicich dotazu (klic firma + cesta + telo).
`patchOn` opravi data v cache z udalosti (`lib/ticketEvents.ts` bere
`payload.ticket`). Rozhrani:
```ts
useApiQuery<T>(path, { refetchOn?, body?, enabled?, patchOn? })
-> { data, loading, refreshing, error, total, reload }
```
**Rozhodnuti majitele produktu: stredni cesta.** Ciselniky (lide, skupiny,
typy ticketu, sluzby, konektory, pristup) jsou v klientskem skladu
`lib/collections.tsx`: nacitaji se line pri prvnim pouziti, mazou se pri
prepnuti firmy a odhlaseni, opravuji se z udalosti entit (upsert nebo smazani
ze zaznamu v payloadu, jinak jedno nacteni te kolekce). Tickety, behy
a statistiky **zustavaji dotazy na server** se strankovanim - jsou velke
a meni se porad. Hooky: `useCollection(key)`, `useAccess()`,
`useCollectionSelector`.
Dalsi opravy klienta:
| Co | Proc |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| 401 maze token a vyvola `auth:expired` | po vyprseni tokenu portal ukazoval prazdne stranky. Login rekne "Prihlaseni vyprselo", stream se prestane pripojovat na 401 a 403 |
| `restore()` maze token jen na 401 | vypadek site pri startu odhlasoval |
| `onClose` modalu v ref | fokus se pri kazdem prekresleni vracel na zacatek |
| toast ma jeden casovac | dva toasty za sebou si rusily odpocet |
| zrusene asynchronni efekty | odpoved pro uz odmontovanou stranku prepisovala stav te nove |
| tiche `catch` nahrazene chybou | pravidlo "zadna ticha selhani" platilo na serveru, na klientovi ne vsude |
| filtry ticketu v URL | nalez slo poslat kolegovi a vratit se pres zpet |
| detail ticketu neblokuje chyba `/people` | jeden padly dotaz na ciselnik schoval cely ticket |
| `MappingEditor` stabilni klice radku | smazani radku prekreslilo vsechny nasledujici a ztratil se kurzor |
| `lib/useUnsavedChanges.ts` | odchod z rozepsaneho builderu bez varovani |
| `DashboardLayout` lazy, sourcemapy vypnute | verejny web nenacital kod portalu, produkce neposila zdrojaky |
| `TicketTable` tabulka nebo karty | `useMediaQuery` misto duplicitni komponenty |
**Builder.** `collectScopes` memoizovane, karty v `memo`, callbacky podle ID
kroku; `FlowCanvas` je rozdeleny do `flow/{ActionCard,ConditionCard,ForeachCard,StepControls}.tsx`
a `canvasTypes.ts`. Stranka Konektory je rozdelena do
`pages/dashboard/connectors/{ConnectorCard,ConnectorEditor,ConnectorLogs,ConnectorTools}.tsx`.
Pred tim byl kazdy stisk klavesy ve strome o padesati krocich prekresleni
vseho.
**Formularova vrstva** podle navrhu v dokumentu 25, sekce 2, je hotova:
`components/ui/form/{controlClass,Field,Input,Select,Textarea}.tsx`,
`lib/useSubmit.ts`, `lib/options.ts`, `components/ui/Chip.tsx`,
`components/dashboard/TicketCard.tsx` (kompaktni varianta), `plural()`
v `lib/format.ts`. Zmizelo 15 kopii trid vstupniho pole.
**Jazyky.** Verejne stranky (Postup, Produkty, Reference, O nas, Kontakt, 404,
Prihlaseni, Sluzby, paticka, navigace) plus `DataState` a `ErrorBoundary` jdou
pres i18n a `en.ts` je pro ne uplna.
**Sdilene typy**: ciste typove moduly v `src/shared/*.ts` (16 souboru,
vcetne `users.ts` pro ucet a clenstvi) jsou jediny zdroj typu API.
`web/src/types/dashboard.ts`, `events.ts` i `AuthContext` je re-exportuji
pres alias `@shared/*` (`web/tsconfig.json` paths a `vite.config.ts` alias).
Pri prevodu se nasly rozjete tvary, vsechny vyresene ve prospech serveru:
webovy `Ticket` nemel `createdById`, `Access.roleNames` bylo nepovinne,
`Person` nemel `enabled`, `Incident` nemel `tenantId` ani `source`, `Service`
neznal kategorii `transformace`, seznam operatoru podminky u typu `list`
na webu nemel `contains`, takze builder nenabizel podminku nad stitky, ktera
na serveru funguje. Serverovy ulozeny tvar (`StoredTicket`, `Connector`
s `values`) zustava na serveru; web dostava `PublicConnector` jako `Connector`.
Pravidlo od ted: novy typ odpovedi patri do `src/shared`, web ho nekopiruje.
### Nova funkce: firma z registru ARES
Zalozit firmu znamenalo opsat nazev, IC, DIC a adresu rucne a pak zvlast
zakladat ucty. Ted je to na `/api/dashboard/settings/ares`, jen pro spravce
platformy:
| Endpoint | Co |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `GET ares/companies?query=` | podle IC presne, jinak podle nazvu. U firmy, ktera uz v portalu je, vraci `existingTenantId` |
| `GET ares/companies/{ico}/persons` | soucasni clenove statutarniho organu a prokura z verejneho rejstriku, kazdy s navrzenym e-mailem `IC-poradi@placeholder.cz` |
| `POST ares/tenants` | zalozi firmu s `ico`, `dic`, `address`, `legalForm` z ARES a ucty vybranych osob, vsechny s roli `role_admin` v nove firme, nahodne heslo, audit `tenant.create.ares` |
Zaznam firmy ma nove nepovinne `ico`, `dic`, `address`, `legalForm`; CRUD
firem hlida unikatni IC. Adresa registru je `ARES_BASE_URL`, vychozi
`https://ares.gov.cz/ekonomicke-subjekty-v-be/rest`. Klient ARES pouziva
tentyz `net/guard.ts` jako vsechno ostatni, co vola ven.
**Rozhodnuti o rolich:** spravce platformy zaklada firmy, spravce firmy pak
spravuje skupiny, vedouci a cleny uvnitr firmy. Rejstrik nezna e-maily, proto
zastupne adresy - **spravce je musi nahradit skutecnymi**, jinak se ti lide
neprihlasi a nedostanou pozvanku.
## 2026-09-08 - Otevrena stranka sekala prehravani videa
Pri otevrenem portalu zacalo vedle nej sekat prehravani videa, po zavreni