Uloziste pro vsechna data, oprava .gitignore, dodelany navrh rozsireni

.gitignore mel vzorec `data/`, ktery se shodl i se `src/data/`. Sestnact
zdrojovych souboru tim tise chybelo v gitu vcetne cele slozky
`src/data/store/`. Opraveno na `/data/`, stejne v .dockerignore.

Tickety vcetne logu, automatizace, incidenty a rozlozeni dashboardu se po
kazde zmene ukladaji. Pomocnik `withMirror` je opak `withCache`: data se meni
v pameti a zapisuji cela, misto aby se po zapisu znovu nacitala. Citace ID se
pri startu dopocitaji z ulozenych zaznamu, takze novy ticket neprepise stary.

Detail ticketu umi typ, tagy, vlastni pole typu a prehozeni na skupinu.
Nastaveni ma prepnuti spravce na jiny ucet, vychozi jen pro cteni.
Skupiny resitelu chodi spolu s lidmi jednim requestem.

Dokumentace: rejstrik znovupouzitelnych funkci (15), navrh monetizace
a ceny za krok (16), popis nastaveni a prav (17). Doplneny endpointy
do openapi.ts, petice CRUD rout se generuje jednou funkci.

Overeno v rezimu souboru: zmeny prezily tvrde ukonceni procesu a po restartu
byly zpatky vcetne logu ticketu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-13 07:46:22 +02:00
co-authored by Claude Opus 5
parent e7cf499a0b
commit afbe948da3
37 changed files with 5594 additions and 106 deletions
+50 -8
View File
@@ -33,17 +33,36 @@ React aplikaci ze slozky `dist/public`.
| Skripty konektoru | hotovo | manifest, kontrola parametru, hot reload, iDoklad |
| Konektory za firmu | hotovo | pristupove udaje v konektoru, overeni napojeni |
| Transformace dat | hotovo | pravidla i sablona JSON, kroky si predavaji struktury |
| Sprava clenstvi z portalu | chybi | memberships jdou zmenit jen v kodu |
| Sprava clenstvi z portalu | hotovo | uzivatele, firmy a role v Nastaveni |
| Role a prava jako data | hotovo | 26 prav v katalogu, vlastni role za firmu |
| Zalozky a limity za firmu | hotovo | navigace chodi ze serveru, ne z kodu klienta |
| Osoby a skupiny resitelu | hotovo | ticket lze prehodit na skupinu, ne jen na cloveka |
| Typy ticketu a vlastni pole | hotovo | typ rozhoduje, ktere akce se na ticketu ukazou |
| Vydefinovane akce na ticketu | hotovo | vazba na typ nebo tag, telo je operace, strom, skript |
| Vlastni widgety | hotovo | zdroj dat z ticketu nebo automatizaci, seskupeni |
| Audit a prepnuti na jiny ucet | hotovo | prepnuti je vychozi jen pro cteni, vse v auditu |
| Bugs a wishes | chybi | vyvojarska agenda, samostatna evidence vedle ticketu |
| Beh automatizaci | chybi | ulozeny strom se nevykonava, neni runtime |
| Uloziste konektoru | hotovo | Postgres, nebo JSON soubor. Udaje vzdy sifrovane |
| Uloziste pro zbytek | chybi | automatizace, rozlozeni a tickety jsou v pameti |
| Uloziste pro zbytek | hotovo | tickety, automatizace, incidenty, rozlozeni, entity |
| Monetizace a cena za krok | navrh | popis v 16-monetizace.md, neni naprogramovane |
| Odesilani e-mailu z formulare | chybi | poptavka se zatim jen loguje |
## Znama omezeni
Data jsou v pameti procesu. Restart containeru vrati tickety, incidenty
i automatizace do vychoziho stavu. Nove vytvorene zaznamy se ztrati.
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 |
Beh automatizaci zatim neexistuje: ulozeny strom se nevykonava, provoz se dela
simulaci. Popis, jak to ma vypadat, je v
[10-runtime-a-kapacita.md](10-runtime-a-kapacita.md).
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
@@ -72,7 +91,30 @@ 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.
Zbytek navrhu je ve dvou souborech, oba jsou navrh k rozhodnuti, ne popis stavu:
[09-navrh-rozsireni.md](09-navrh-rozsireni.md) pro datove modely a prava,
[10-runtime-a-kapacita.md](10-runtime-a-kapacita.md) pro frontu, beh kroku
a rozpocet na 150 klientu.
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)
pro frontu, beh kroku a rozpocet na 150 klientu, a
[16-monetizace.md](16-monetizace.md) pro cenu za krok.
## Dokumentace
| Soubor | O cem |
| --- | --- |
| [02-appfactory-proxy.md](02-appfactory-proxy.md) | beh za reverse proxy, ROOT_PATH, health |
| [03-architektura-a-mapa-kodu.md](03-architektura-a-mapa-kodu.md) | kde co je |
| [04-api.md](04-api.md) | endpointy a to, co ze Swaggeru neni videt |
| [05-dashboard-a-builder.md](05-dashboard-a-builder.md) | editor automatizaci |
| [06-tickety.md](06-tickety.md) | model ticketu a log prubehu |
| [07-firmy-a-prava.md](07-firmy-a-prava.md) | firmy, pohledy, kdo co vidi |
| [08-dashboard-widgety.md](08-dashboard-widgety.md) | nastavitelny prehled |
| [09-navrh-rozsireni.md](09-navrh-rozsireni.md) | puvodni navrh rozsireni |
| [10-runtime-a-kapacita.md](10-runtime-a-kapacita.md) | **navrh**: fronta, beh kroku, kapacita |
| [11-skripty-konektoru.md](11-skripty-konektoru.md) | vykonna cast sluzeb |
| [12-sluzby-a-konektory.md](12-sluzby-a-konektory.md) | sluzba, konektor, viditelnost |
| [13-transformace-dat.md](13-transformace-dat.md) | pole na pole a JSON na JSON |
| [14-databaze.md](14-databaze.md) | tri rezimy uloziste, migrace, sifrovani |
| [15-rejstrik-funkci.md](15-rejstrik-funkci.md) | k cemu je jaka funkce a komponenta |
| [16-monetizace.md](16-monetizace.md) | **navrh**: cena za krok a balicky |
| [17-nastaveni-a-prava.md](17-nastaveni-a-prava.md) | prava, typy, akce, widgety, prepnuti uctu |
| [99-zmeny.md](99-zmeny.md) | zaznam zmen, nejnovejsi nahore |
+57
View File
@@ -36,6 +36,11 @@ Vyzaduji `Authorization: Bearer <token>`:
| POST | `/api/dashboard/tickets/:id/assign` |
| POST | `/api/dashboard/tickets/:id/status` |
| POST | `/api/dashboard/tickets/:id/comment` |
| POST | `/api/dashboard/tickets/:id/type` |
| POST | `/api/dashboard/tickets/:id/tags` |
| POST | `/api/dashboard/tickets/:id/group` |
| GET | `/api/dashboard/tickets/:id/actions` |
| POST | `/api/dashboard/tickets/:id/actions/:actionId` |
| GET | `/api/dashboard/incidents` |
| GET | `/api/dashboard/storage` |
| GET | `/api/dashboard/services` |
@@ -58,8 +63,26 @@ Vyzaduji `Authorization: Bearer <token>`:
| PUT | `/api/dashboard/automations/:id` |
| DELETE | `/api/dashboard/automations/:id` |
| POST | `/api/dashboard/automations/:id/webhook/regenerate` |
| POST | `/api/dashboard/widget-data` |
| GET | `/api/dashboard/widget-data/options` |
| GET | `/api/dashboard/settings/catalog` |
| GET | `/api/dashboard/settings/features` |
| GET | `/api/dashboard/settings/people-overview` |
| GET | `/api/dashboard/settings/users-overview` |
| PATCH | `/api/dashboard/settings/users/:id/password` |
| POST | `/api/admin/impersonate` |
| POST | `/api/admin/impersonate/stop` |
| GET | `/api/admin/impersonate/candidates` |
| GET | `/api/admin/audit` |
| POST | `/api/simulate` |
Sprava zaznamu ma u kazde entity stejnou petici (seznam, detail, vytvoreni,
uprava, mazani) na `/api/dashboard/settings/<entita>`, protoze ji dela jedna
fabrika (`src/routes/crud.ts`):
`tenants`, `users`, `roles`, `people`, `groups`, `ticket-types`, `actions`,
`widgets`, `features`.
## Format chyb
Jednotny pro cele API:
@@ -165,6 +188,40 @@ toho, co ktera volana sluzba vratila.
`POST /api/dashboard/tickets/:id/assign` s telem `{"assigneeId": null}` vrati
ticket do fronty. Neznamy resitel vraci 404, ne tiche odpojeni.
## Akce na ticketu
Popis modelu je v [09-navrh-rozsireni.md](09-navrh-rozsireni.md).
`GET /api/dashboard/tickets/:id/actions` vraci **jen akce, ktere v teto situaci
opravdu jdou spustit**: sedi typ nebo tag, projdou podminky a volajici na ne ma
pravo. Klient si nefiltruje nic - jinak by se to pocitalo na dvou mistech
a jednou se to rozejde.
`POST /api/dashboard/tickets/:id/actions/:actionId` vraci **200 i kdyz akce
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.
## 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.
`GET /api/dashboard/settings/catalog` vraci katalog prav a modulu, aby formular
role nemel seznam prav napsany v kodu klienta.
## Prepnuti na jiny ucet
`POST /api/admin/impersonate` vraci novy token s narokem `act` (kdo se za koho
vydava) a `writes`. Bez `writes` middleware **odmitne cokoliv jineho nez GET**
s 403. Kazde prepnuti i ukonceni je v auditu vcetne toho, kdo to byl doopravdy.
Svuj puvodni token si klient odklada do `sessionStorage`, server o nem nic nevi.
## Simulace
`POST /api/simulate` vyvola provozni udalost pro nahled ziveho dashboardu.
+86
View File
@@ -0,0 +1,86 @@
# Rejstřík znovupoužitelných funkcí a komponent
K čemu je co, aby se za rok nepsalo znovu něco, co už existuje. Když píšeš
druhou funkci, která dělá skoro totéž jako něco odsud, je to skoro vždycky
chyba - buď se má použít ta původní, nebo se má rozšířit.
Podrobný popis je vždycky v komentáři u samotné funkce. Tady je jen jedna věta
a kdy to použít.
## Ukládání dat (server)
Rozhoduje se na **jednom místě**, viz [14-databaze.md](14-databaze.md).
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. |
| `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í). |
| `isVisible(entity, options)` | `src/data/store/types.ts` | Vidí volající tenhle záznam? Prázdný seznam firem znamená "nic", ne "vše". |
| `nowIso()` | `src/data/store/types.ts` | Časová značka. Ať se nepíše `new Date().toISOString()` na třiceti místech. |
| `memorySnapshot` / `fileSnapshot` | `src/data/snapshot.ts` | Nižší vrstva pod `createLocalStore`: atomický zápis JSONu s debounce. Přímo se nepoužívá. |
| `db()`, `query`, `queryOne`, `transaction` | `src/db/pool.ts` | Postgres. `dbFor(tenantId)` je připravený šev pro rozdělení na víc databází. |
| `seal`, `open`, `sealAll`, `openAll` | `src/db/secretBox.ts` | Šifrování přístupových údajů konektorů (AES-256-GCM). Nic tajného se neukládá jinak. |
| `runMigrations()` | `src/db/migrate.ts` | Migrace pod zámkem, jeden soubor = jedna transakce. |
## Entity a práva (server)
| Co | Kde | K čemu |
| --- | --- | --- |
| `crudRouter(options)` | `src/routes/crud.ts` | Celý CRUD nad jednou entitou: seznam, detail, vytvoření, úprava, mazání, právo, audit. Nová entita v nastavení = jeden `crudRouter`, ne pět handlerů. |
| `readScope(req)` | `src/routes/crud.ts` | Ze které firmy smí request číst. Povinný argument všech `list` volání. |
| `accessFor(user, tenantId?)` | `src/data/access.ts` | Co uživatel smí: práva, záložky, výchozí firma. Klient si nic nedovozuje sám. |
| `permissionsOf(user, tenantId)` | `src/data/permissions.ts` | Efektivní práva z rolí. Pětisekundová cache, `invalidatePermissions()` po zápisu. |
| `hasPermission(...)` | `src/data/permissions.ts` | Jedna kontrola. Používá ji `crudRouter` i ruční handlery. |
| `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. |
| `refreshCaches()` | `src/data/bootstrap.ts` | Obnoví všechny kopie v paměti. Volá se po zápisu, který je může změnit. |
| `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)
Viz [11-skripty-konektoru.md](11-skripty-konektoru.md).
| Co | Kde | K čemu |
| --- | --- | --- |
| `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. |
| `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. |
| `createHttp(...)` | `src/scripts/http.ts` | HTTP se timeoutem, limitem odpovědi a rozlišením "zkusit znovu" a "marné". |
| `scriptIdFor(serviceId, operationId)` | `src/scripts/lookup.ts` | Který skript obsluhuje operaci z katalogu. |
## Klient
| Co | Kde | K čemu |
| --- | --- | --- |
| `EntityAdmin` | `components/dashboard/EntityAdmin.tsx` | Celá správa jedné entity: tabulka, modál, validace, mazání. Nová záložka nastavení = popis sloupců a polí, ne nová stránka. |
| `parseJsonField` | `components/dashboard/EntityAdmin.tsx` | Textové pole s JSONem na hodnotu, s hlášením, kde je chyba. |
| `TicketActions` | `components/dashboard/TicketActions.tsx` | CTA akcí na ticketu plus typ, tagy a vlastní pole. Seznam akcí chodí ze serveru už vyfiltrovaný. |
| `ErrorDetail` | `components/dashboard/ErrorDetail.tsx` | Rozbalovací celé chybové hlášení s kopírováním. Chyba se nikdy nezkracuje. |
| `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`. |
| `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. |
| `usePageMeta` | `lib/usePageMeta.ts` | Titulek stránky. |
| `Badge`, `Button`, `Modal`, `Card`, ... | `components/ui/` | Základní prvky. Nový vzhled tlačítka patří sem, ne do stránky. |
## Pravidla, která z toho plynou
1. **Nová entita v nastavení**: `defineStore` v modulu entity, řádek v
`bootstrap.ts`, `crudRouter` v `settings.ts`, popis v `Settings.tsx`.
Nic jiného se psát nemusí.
2. **Data, která se mění za provozu**, jdou přes `withMirror`. Data, která se
čtou při každém requestu a mění zřídka, přes `withCache`. Obojí nikdy.
3. **Chybu se nesmí zkracovat.** Server vrací celé hlášení, klient ho umí
zobrazit (`ErrorDetail`).
4. **Klient nepočítá práva.** Co smí, říká `accessFor`.
+95
View File
@@ -0,0 +1,95 @@
# Návrh monetizace a cena za krok
Stav: **návrh**, není naprogramované. Zbytek dokumentace popisuje hotové věci,
tenhle soubor ne - viz [99-zmeny.md](99-zmeny.md).
Zadání bylo: chci si napsat vlastní automatizaci a hned vidět, kolik by mě
měsíčně stála, když si k jednotlivým krokům zadám ceny (například iDoklad,
odeslání objednávky, 0,10 Kč).
## Co se dá účtovat
Čtyři věci, každá měří něco jiného:
| Základ | Co to znamená | Proč / proč ne |
| --- | --- | --- |
| Za uživatele | Kolik lidí má přístup do portálu | Předvídatelné, ale nesouvisí s tím, co aplikace dělá. U automatizací platí zákazník za lidi, kteří tam nemusí chodit. |
| Za automatizaci | Kolik má zapnutých stromů | Trestá to rozdělení jednoho velkého stromu na tři přehledné. Špatná motivace. |
| **Za krok** | Kolik kroků se skutečně vykonalo | Odpovídá naší práci: každý krok je jedno volání služby, jeden zápis, jeden běh skriptu. Zákazník vidí, za co platí. |
| Za objem dat | Kolik toho proteče | Nesouvisí s náklady, u nás jsou to kilobajty. |
Doporučení: **paušál plus kroky**. Paušál kryje portál, tickety, úložiště
a podporu, kroky kryjí provoz automatizací. Bez paušálu je zákazník, který má
automatizace vypnuté, zdarma, a přesto mu běží ticketovací systém.
## Ceník kroků
Cena patří **k operaci v katalogu**, ne ke službě. Zápis faktury do iDokladu
a jeho dotaz na kontakt stojí jinak, protože nás jinak stojí.
Tři pásma:
| Pásmo | Příklady | Návrh ceny |
| --- | --- | --- |
| Obecné | pauza, zápis do logu, podmínka, transformace dat | 0 Kč. Účtovat podmínku je jako účtovat mezeru v textu. |
| Naše práce | webhook, plánovač, založení ticketu, přiřazení řešitele, HTTP požadavek | 0,02 Kč |
| Cizí služba | iDoklad, Shoptet, SAP, CRM, hlasová brána, AI | 0,05 až 0,50 Kč podle toho, co za to platíme sami |
Číslo je jedno pole u operace, takže se dá měnit bez zásahu do kódu. Ceník
je verzovaný: změna ceny nepřepíše historii, jinak by se zpětně změnila
i vyúčtovaná částka.
## Odhad ceny v editoru
Co k tomu je potřeba:
1. **Cena u operace.** Do katalogu (`src/data/services.ts`) přidat
`priceCzk` k `ServiceOperation`. Chybějící cena znamená 0, ne chybu -
nová operace nesmí rozbít odhad.
2. **Očekávaný počet běhů.** Jedno číslo u automatizace, které zadá uživatel
("čekám 400 objednávek denně"). Bez něj nejde nic spočítat a hádat to za
uživatele by dalo číslo, kterému nebude věřit.
3. **Součet přes strom.** Podmínka má dvě větve a projde se vždycky jen jedna.
Pro odhad se bere **dražší** větev, aby výsledek byl horní hranice, ne
příjemné číslo, které se pak překročí. Vedle se ukáže i levnější varianta.
4. **Zobrazení.** V editoru u každého kroku jeho cena, nahoře součet za běh
a za měsíc. Jedna funkce, obě čísla, žádný druhý výpočet na klientovi.
Odhad je záměrně jen odhad: skutečná fakturace se počítá ze **skutečně
vykonaných kroků** zapsaných při běhu, ne z toho, co editor předpověděl.
## Skutečné vyúčtování
Každý vykonaný krok už teď zapisuje řádek do logu ticketu nebo běhu
automatizace. Na fakturaci z toho chybí:
- u řádku evidovat firmu, operaci a cenu **platnou v tu chvíli**,
- denní součet za firmu (aby se faktura nepočítala přes miliony řádků),
- měsíční uzávěrka, která součty zamkne.
Model je stejný jako u auditu: připisovací tabulka, nic se nepřepisuje.
Uzavřený měsíc se nedá změnit, jen opravit dobropisem.
## Balíčky
Cena za krok samotná zákazníka děsí, protože nezná svoje čísla. Proto balíčky
s předplacenými kroky a stejnou cenou nad limit:
| Balíček | Paušál | Kroků v ceně | Nad limit |
| --- | --- | --- | --- |
| Start | 490 Kč | 5 000 | 0,05 Kč |
| Provoz | 1 900 Kč | 40 000 | 0,04 Kč |
| Firma | 6 900 Kč | 200 000 | 0,03 Kč |
Nad limit se **nevypíná**. Zastavit zákazníkovi fakturaci objednávek kvůli
překročení limitu je horší než mu to dofakturovat. Limit hlásí varování
v portálu a e-mailem.
## Co je potřeba rozhodnout
1. Účtují se kroky, které skončily chybou? Návrh: **ne u naší chyby, ano
u chyby cizí služby**, protože volání jsme opravdu zaplatili. Musí to být
v ceníku vidět, jinak to vypadá jako počítání chyb ve svůj prospěch.
2. Účtuje se opakování po chybě? Návrh: první opakování zdarma, další ano.
3. Účtují se kroky v testovacím režimu? Návrh: ne, ale s denním limitem, aby
se testem ceník neobcházel.
+128
View File
@@ -0,0 +1,128 @@
# Nastavení, práva, typy ticketů, akce a widgety
Co všechno se dá nastavit z portálu a proč je to postavené takhle. Model je
z [09-navrh-rozsireni.md](09-navrh-rozsireni.md), tady je hotový stav.
Seznam funkcí a komponent, které se u toho mají použít, je
v [15-rejstrik-funkci.md](15-rejstrik-funkci.md).
## Jedna vrstva pro všechny záznamy
Firmy, uživatelé, role, řešitelé, skupiny, typy ticketů, akce a widgety mají
společné to, že se u nich dělá totéž: seznam za firmu, detail, zápis, mazání,
kontrola práva, zápis do auditu. Napsat to osmkrát znamená osm skoro stejných
souborů, ze kterých se jeden opraví a ostatní ne.
Proto tři vrstvy, každá napsaná jednou:
| Vrstva | Kde | Co dělá |
| --- | --- | --- |
| Úložiště | `src/data/store/` | `EntityStore<T>` a dvě implementace. Volající nepozná, jestli běží Postgres nebo JSON soubor. |
| API | `src/routes/crud.ts` | `crudRouter` vyrobí pětici endpointů včetně práva a auditu. |
| Klient | `components/dashboard/EntityAdmin.tsx` | Tabulka, modál, validace, mazání. |
Nová entita v nastavení pak znamená: `defineStore` v modulu entity, jeden řádek
v `bootstrap.ts`, jeden `crudRouter` v `settings.ts`, jeden popis v
`Settings.tsx`. Nic víc.
## Práva jsou data
Práv je 26 a jsou v katalogu (`src/data/permissions.ts`). Role je **záznam**,
ne konstanta v kódu: firma si může udělat vlastní roli s vlastní kombinací
práv. Systémové role (`admin`, `agent`, `viewer`) se měnit nedají, aby si nikdo
neodebral právo, kterým je odebírá.
Členství uživatele ve firmě nese `roleIds`, protože jeden člověk může být
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í.
## Navigace chodí ze serveru
Co uživatel vidí za záložky, je průnik dvou věcí:
1. co má firma zaplacené a zapnuté (`tenantFeatures`),
2. na co má člověk právo.
Počítá to server (`navFor`) a klient jen kreslí. Kdyby si klient záložky
dovozoval sám, počítalo by se to na dvou místech a jednou by se to rozešlo -
a ta chyba by znamenala odkaz do 403.
## Typy ticketů a tagy
Typ ticketu nese **vlastní pole** (klíč, popisek, typ, povinnost, nápověda)
a může mít vlastní workflow stavů. Tag je volné označení bez polí za ním.
Rozdíl není kosmetický:
- **Typ** znamená "za tímhle stojí data". Objednávka má číslo objednávky
a částku, takže se na ni dá navázat akce, která ta data potřebuje.
- **Tag** znamená "takhle si to označuju". Hodí se na filtry a widgety.
Akce se proto váže na **typ nebo tag**, ale akci s daty má smysl vázat na typ.
Pole `object` a `list` se v detailu ticketu ručně nevyplňují - plní je
automatizace. Textarea s JSONem by tam byla past, ne pohodlí.
## Vydefinované akce
Akce a automatizační stromy jsou **dvě různé věci** a nemají mezi sebou žádnou
společnou mezivrstvu. Automatizace běží sama, akce je tlačítko, které zmáčkne
člověk.
Tělo akce je jedno z trojice:
| Tělo | Kdy | Příklad |
| --- | --- | --- |
| Operace konektoru | běžný případ | odeslat objednávku do iDokladu |
| Vlastní strom | akce má víc kroků a rozhodování | dohledat kontakt, vystavit fakturu, odeslat e-mailem |
| Skript | nic z toho nestačí | vlastní výpočet nebo cizí API, které v katalogu není |
Server vrací k ticketu **jen akce, které v té situaci opravdu jdou spustit**:
sedí typ nebo tag, projdou podmínky a volající na ně má právo. Klient
nefiltruje nic.
Spuštění akce vrací 200 i když akce selhala - selhání akce není chyba API.
V odpovědi je celé chybové hlášení a celý průběh se zapíše do logu ticketu.
Vestavěné akce (typ, tagy, skupina, přiřazení, stav, komentář) jsou zvlášť:
mění ticket sám, ne cizí službu, a každá má vlastní právo.
## Vlastní widgety
Widget je definice zdroje dat plus způsob zobrazení. Zdroj je ticket nebo
automatizace, k tomu filtr a případné seskupení (podle stavu, kanálu, řešitele,
typu, tagu).
Data pro celý přehled chodí **jedním requestem**. Deset dlaždic nesmí znamenat
deset dotazů.
Viditelnost je stejná jako u služeb: všichni, konkrétní firmy, konkrétní lidé,
nebo jen správce.
## Přepnutí na jiný účet
Správce platformy se může podívat na portál očima konkrétního uživatele.
Dvě věci, na kterých to stojí:
1. **Výchozí je jen pro čtení.** Podívat se, co klient vidí, je mnohem
častější než za něj něco měnit, a nechtěný zápis pod cizím jménem je to
nejhorší, co se tady může stát. Zápis se musí zapnout vědomě a middleware
ho jinak odmítne u všeho kromě GET.
2. **Všechno je v auditu** včetně toho, kdo to doopravdy byl. Bez auditu nemá
přepínání účtů co dělat v produktu.
Svůj původní token si klient odkládá do `sessionStorage`. Server o něm nic neví,
takže ho nemůže ani omylem prodloužit.
## Co se ukládá a co se drží v paměti
| 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. |
| 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
nikdy nepřepíše starý.
+54
View File
@@ -2,6 +2,60 @@
Nejnovejsi nahore.
## 2026-08-13 - firmy, prava, typy ticketu, akce, widgety a uloziste pro vsechno
Dodelany cely [navrh rozsireni](09-navrh-rozsireni.md) a vsechna data se
ukladaji. Rejstrik novych funkci a komponent je v
[15-rejstrik-funkci.md](15-rejstrik-funkci.md).
### Pridano - obecne vrstvy
- `src/data/store/`: jedno rozhrani `EntityStore<T>` a dve implementace, soubor
nebo pamet (`local.ts`) a Postgres nad tabulkou `records` (`postgres.ts`).
Volajici nepozna, ktera bezi. Nova entita znamena jeden `defineStore` a jeden
radek v `bootstrap.ts`.
- `store/cached.ts` (`withCache`) pro konfiguracni entity ctene pri kazdem
requestu a `store/mirror.ts` (`withMirror`) pro provozni data, ktera se meni
v pameti a po zmene se cela zapisuji. Dva pomocniky, ne osm skoro stejnych
souboru.
- `put` v rozhrani uloziste: zapis celeho zaznamu bez skladani patche.
- `src/routes/crud.ts`: fabrika CRUD rout. Seznam, detail, zapis, mazani, pravo
a audit na jednom miste.
- `components/dashboard/EntityAdmin.tsx`: obecna sprava zaznamu na klientovi.
Nova zalozka nastaveni je popis sloupcu a poli, ne nova stranka.
### Pridano - funkce
- Firmy, uzivatele, clenstvi, osoby a skupiny resitelu se spravuji z portalu.
- Role a prava jsou **data**, ne pevny seznam v kodu: katalog 26 prav, vlastni
role za firmu, systemove role nejde menit. Navigace chodi ze serveru jako
prunik toho, co firma ma, a toho, na co ma clovek pravo.
- Typy ticketu s vlastnimi poli, tagy a vlastni workflow stavu.
- Vydefinovane akce na ticketu: vazba na **typ nebo tag**, telo je jedna
operace, vlastni strom, nebo skript. Server posila jen akce, ktere v dane
situaci projdou podminkami a pravy - klient si nepocita, co ukazat.
- Vlastni widgety dashboardu vcetne dat na jeden request a seskupeni.
- Audit a prepnuti spravce na jiny ucet. Prepnuti je **vychozi jen pro cteni**,
zapis se musi zapnout vedome a je videt v auditu.
- Detail ticketu: CTA akci, typ, tagy, vlastni pole a prehozeni na skupinu.
### Pridano - uloziste pro provozni data
Tickety vcetne logu, automatizace, incidenty a rozlozeni dashboardu se po kazde
zmene zapisuji. Citace ID se pri startu dopocitaji z ulozenych zaznamu, takze
novy ticket nikdy neprepise stary.
### Overeno
V rezimu `file`: zmena typu, tagu, vlastnich poli, stavu, komentare, nova
automatizace, novy incident a upravene rozlozeni dashboardu prezily **tvrde
ukonceni procesu** a po restartu byly zpatky vcetne logu ticketu. Novy ticket
dostal dalsi cislo, ne cislo existujiciho. Agent bez prav dostane 403 na spravu
roli a uzsi navigaci. `passwordHash` se nikdy nevraci.
Databaze se v tomhle kole neoverovala, nebylo na cem - kod pro ni je stejny
a psany soucasne, ale nebezel.
## 2026-08-12 - soubor jako uloziste bez databaze
Mockup se k databazi nedostane, takze pribyl treti rezim: JSON soubor.