updated tickets

This commit is contained in:
JiriUhlir
2026-08-03 11:45:35 +02:00
parent 9a0c67af08
commit 52b190bfbc
28 changed files with 3228 additions and 241 deletions
+15 -4
View File
@@ -17,9 +17,14 @@ React aplikaci ze slozky `dist/public`.
| Dashboard | hotovo | prehled, tickety, incidenty, automatizace, nastaveni |
| Zivy dashboard pres SSE | hotovo | zmeny se projevi bez obnoveni stranky |
| Simulace provozu | hotovo | tlacitko v postrannim menu portalu |
| Katalog konektoru | hotovo | 25 sluzeb, 8 kategorii |
| Katalog konektoru | hotovo | 26 sluzeb, 9 kategorii |
| Builder automatizaci | hotovo | strom akci, vetveni podminkou |
| Webhook s registrovanou adresou | hotovo | token generuje server, verejny endpoint validuje data |
| Tickety na konkretni lidi | hotovo | resitel, filtr moje, prehled vytizeni tymu |
| Log ticketu ve strome | hotovo | vcetne toho, co ktera sluzba vratila |
| Kanaly do ticketu | hotovo | WhatsApp, e-mail, hlas a formular jako spoustece |
| Parametry od sluzby | hotovo | katalog je deklaruje, server je dosazuje pri ulozeni |
| Bugs a wishes | chybi | vyvojarska agenda, samostatna evidence vedle ticketu |
| Nastaveni poli akci | chybi | akce zatim neumi cerpat z parametru spoustece |
| Beh automatizaci | chybi | ulozeny strom se nevykonava, neni runtime |
| Databaze | chybi | data jsou v pameti, restart je vrati na vychozi stav |
@@ -37,9 +42,15 @@ miste v `web/src/config/brand.ts`.
Zivy stream drzi seznam posluchacu v pameti jedne instance. Pri vice instancich
by ho musel nahradit sdileny kanal, napriklad Redis pub/sub.
Log ticketu zatim plni simulace, ne skutecny beh. Zaznamy jsou realisticke,
ale nevznikly vykonanim ulozeneho stromu - runtime neexistuje.
## Dalsi krok
Nejuzitecnejsi pristavek je nastaveni poli akci a s nim predavani dat mezi kroky,
aby slo rict "do e-mailu dej parametr customer ze spoustece". Je to zasah do
datoveho modelu, vyplati se navrhnout drive nez se builder rozsiri dal.
Podrobnosti v [05-dashboard-a-builder.md](05-dashboard-a-builder.md).
aby slo rict "do e-mailu dej parametr customer ze spoustece". Parametry spoustece
uz existuji na obou stranach, chybi jen jejich pouziti v akcich. Podrobnosti
v [05-dashboard-a-builder.md](05-dashboard-a-builder.md).
Vedle toho stoji za rozmysleni evidence bugs a wishes. Zamerne to nejsou tickety,
duvod je v [06-tickety.md](06-tickety.md).
+27
View File
@@ -48,6 +48,33 @@ Vite build ma `base: './'`, tedy relativni odkazy na soubory. Server pri odeslan
Bez `<base>` by prohlizec hledal soubory v `/apps/<app-id>/dashboard/assets/...`
a dostal by HTML aplikace misto skriptu.
## Koncove lomitko v adrese aplikace
Aplikaci je nutne otevirat **s koncovym lomitkem**:
```
https://services.csbot.cz/apps/<app-id>/ funguje
https://services.csbot.cz/apps/<app-id> vraci prazdnou odpoved
```
Neni to chyba aplikace. Caddy routuje pres `handle_path /apps/<app-id>/*`,
coz sedi na cestu s lomitkem a na vse pod ni, ale ne na holou cestu bez nej.
Pozadavek se pak do containeru vubec nedostane.
Pozna se to podle hlavicek. Odpoved z aplikace ma `X-Powered-By: Express`
a `Content-Type`, kdezto prazdna odpoved ma jen `Server: Caddy`:
```bash
curl -sI https://services.csbot.cz/apps/<app-id> | grep -i x-powered-by # nic
curl -sI https://services.csbot.cz/apps/<app-id>/ | grep -i x-powered-by # Express
```
Ostatni cesty (`/health`, `/docs/`, `/api/...`) lomitko v sobe uz maji,
takze se jich to netyka.
Opravit se to da jen presmerovanim v konfiguraci Caddy, tedy v repozitari
AppFactory. Z aplikacniho repozitare se infrastruktura nemeni, viz AGENTS.md.
## Povinne endpointy
| Verejna cesta | Vraci |
+11 -1
View File
@@ -39,7 +39,8 @@ image jen `dist`, takze staci jedna slozka.
| `src/routes/simulate.ts` | vyvolani provoznich udalosti |
| `src/routes/webhook.ts` | verejny prijem dat do automatizace |
| `src/routes/contact.ts` | poptavkovy formular z webu |
| `src/data/ticketStore.ts` | tickety vcetne zmen a udalosti |
| `src/data/ticketStore.ts` | tickety, jejich resitele, log prubehu, prehled vytizeni |
| `src/data/people.ts` | resitele ticketu - oddeleni od uzivatelu portalu |
| `src/data/incidentStore.ts` | incidenty vcetne zmen a udalosti |
| `src/data/automationStore.ts` | automatizace, strom akci, tokeny webhooku |
| `src/data/connectors.ts` | katalog konektoru, jejich spousteču a akci |
@@ -61,6 +62,8 @@ image jen `dist`, takze staci jedna slozka.
| `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 a vyber kroku |
| `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 |
@@ -82,4 +85,11 @@ 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.
**Resitel neni uzivatel.** Uzivatel se prihlasuje do portalu, resitel ma u sebe
tickety. Technik muze mit tickety a ucet nikdy nemit. Spojka je e-mail,
podrobnosti v [06-tickety.md](06-tickety.md).
**Filtrovani ticketu dela server.** Klient posila query parametry a dostane hotovy
seznam. Kdyby filtroval sam, ukazoval by jina cisla nez prehled vytizeni.
**Zadna ticha selhani.** Kazdy `catch` loguje a uzivatel se o chybe dozvi.
+35 -2
View File
@@ -23,7 +23,13 @@ Vyzaduji `Authorization: Bearer <token>`:
| GET | `/api/auth/me` |
| POST | `/api/auth/logout` |
| GET | `/api/dashboard/summary` |
| GET | `/api/dashboard/people` |
| GET | `/api/dashboard/tickets` |
| GET | `/api/dashboard/tickets/workload` |
| GET | `/api/dashboard/tickets/:id` |
| POST | `/api/dashboard/tickets/:id/assign` |
| POST | `/api/dashboard/tickets/:id/status` |
| POST | `/api/dashboard/tickets/:id/comment` |
| GET | `/api/dashboard/incidents` |
| GET | `/api/dashboard/connectors` |
| GET | `/api/dashboard/stream` |
@@ -72,8 +78,8 @@ cookie se `Secure` a `SameSite` plus CSRF token.
a poslednich par udalosti, pak uz jen nove. Kazdych 25 sekund jde komentarovy
radek, aby spojeni neuspalo proxy.
Typy udalosti: `ticket.created`, `ticket.updated`, `ticket.resolved`,
`incident.started`, `incident.updated`, `incident.resolved`,
Typy udalosti: `ticket.created`, `ticket.updated`, `ticket.assigned`,
`ticket.resolved`, `incident.started`, `incident.updated`, `incident.resolved`,
`automation.created`, `automation.updated`, `automation.deleted`,
`automation.run`, `webhook.received`.
@@ -107,6 +113,29 @@ curl -X POST https://services.csbot.cz/apps/<app-id>/webhook/<token> \
Prototyp pozadavek prijme, zvaliduje a zapocita do metrik, ale strom akci
nevykona - runtime neexistuje.
## Tickety
Popis modelu je v [06-tickety.md](06-tickety.md), tady jen to, co se tyka API.
`GET /api/dashboard/tickets` bere filtry v query: `assignee`, `status`, `channel`.
U `assignee` jsou dve zvlastni hodnoty: `me` znamena resitele odpovidajiciho
prihlasenemu uzivateli, `unassigned` frontu bez resitele. Neznama hodnota filtru
se zaloguje a ignoruje - je lepsi ukazat vic ticketu nez prazdny seznam
bez vysvetleni.
Odpoved nese vedle `items` jeste `meId`. Klient podle nej pozna, ktere tickety
jsou jeho, a jestli ma vubec smysl nabizet filtr "moje".
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.
`GET /api/dashboard/tickets/:id` vraci navic `trace`, tedy log prubehu vcetne
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.
## Simulace
`POST /api/simulate` vyvola provozni udalost pro nahled ziveho dashboardu.
@@ -115,6 +144,10 @@ Zamerne meni skutecna data, ne jen posila falesnou notifikaci.
Akce: `ticket.created`, `ticket.resolved`, `incident.started`,
`incident.resolved`, `automation.run`.
U `ticket.created` urcuje `channel`, odkud pozadavek prisel, a podle toho se
poskladá i log ticketu. `knownCustomer: false` znamena, ze CRM firmu nedohleda -
ticket zustane bez zakaznika i bez resitele a v logu je videt proc.
Nevyplnena pole server doplni ukazkovou hodnotou. U akci s "resolved" se bez
zadaneho id pouzije prvni nevyrizeny zaznam.
+20 -5
View File
@@ -7,7 +7,8 @@
/dashboard/automatizace seznam a zalozeni nove
/dashboard/automatizace/:id builder: strom akci
/dashboard/konektory katalog sluzeb, jejich spousteču a akci
/dashboard/tickety tabulka ticketu
/dashboard/tickety seznam, filtry, prehled vytizeni tymu
/dashboard/tickety/:id detail ticketu: prubeh a log, resitel, zakaznik
/dashboard/incidenty prehled incidentu
/dashboard/nastaveni udaje o uctu
```
@@ -33,7 +34,8 @@ az do 15 sekund, aby pri vypadku serveru neubijel provoz.
Modalni okno se otevre tlacitkem Simulace. Umoznuje:
- zalozit ticket s vlastnim predmetem, zadavatelem a prioritou,
- zalozit ticket z vybraneho kanalu vcetne celeho logu, ktery k nemu vede,
a s prepinacem, jestli se zakaznik v CRM dohleda nebo ne,
- vyvolat incident s vlastnim popisem, sluzbou a zavaznosti,
- vyresit prvni nevyrizeny ticket nebo bezici incident,
- spustit automatizaci uspesne nebo s chybou.
@@ -69,11 +71,21 @@ existujici podminky nerozbije.
Misto vlozeni urcuje `FlowPath` v `web/src/lib/flow.ts`: prazdne pole je hlavni
sekvence, `[{ stepId, branch }]` je vetev konkretni podminky.
## Webhook a vstupni parametry
## Odkud se berou vstupni parametry
Jsou dva druhy spousteču a lisi se tim, kdo urcuje jejich parametry.
**Parametry deklaruje uzivatel** (webhook, formular). V katalogu maji
`customPayload: true`. V builderu se pridavaji rucne: nazev, typ (text, cislo,
ano-ne, datum) a povinnost.
**Parametry urcuje sluzba** (e-mail, WhatsApp, hlasova linka, ticket). V katalogu
je nese `providedFields`. Builder je ukazuje jen ke cteni a server je pri ulozeni
stromu vzdy dosadi z katalogu, jeste pred validaci. Podrobnosti a duvody jsou
v [06-tickety.md](06-tickety.md), sekce "Parametry od sluzby".
U spoustece typu webhook vygeneruje server pri ulozeni adresu
`POST <verejna-adresa>/webhook/<token>`. U spoustece se deklaruji vstupni
parametry: nazev, typ (text, cislo, ano-ne, datum) a povinnost.
`POST <verejna-adresa>/webhook/<token>`.
Podminky pak porovnavaji hodnotu parametru, napriklad `score >= 15`. Nabidka
operatoru se ridi typem, na cislo nejde pustit "obsahuje". Tabulka operatoru je
@@ -104,6 +116,8 @@ Rozdelana prace se nikdy nezahazuje.
Musi existovat v `lucide-react`.
3. Pokud patri do nove kategorie, doplnit ji do `connectorCategories` a do typu
`ConnectorCategory` na obou stranach.
4. Pokud spoustec predava vlastni data, deklarovat je v `providedFields`. ID
parametru musi zustat stabilni, odkazuji se na ne podminky v ulozenych stromech.
Builder i katalog ji vezmou automaticky.
@@ -112,6 +126,7 @@ Builder i katalog ji vezmou automaticky.
| Chybi | Poznamka |
| --------------------------- | -------------------------------------------------------- |
| Nastaveni poli akci | `fields` u akci se zobrazuji jen jako napoveda |
| Vazba logu ticketu na beh | log plni simulace, ne vykonany strom |
| Predavani dat do akci | chybi syntaxe odkazu, navrh je `{{trigger.customer}}` |
| Kombinovane podminky | jedna podminka je jedno porovnani, AND a OR jen vnorenim |
| Beh automatizaci | ulozeny strom se nevykonava |
+185
View File
@@ -0,0 +1,185 @@
# 06 - Tickety
## Co ticket je a co neni
Ticket je **prichozi pozadavek odkudkoliv**. Prisla zprava na WhatsApp, prisel
e-mail, nekdo zavolal na hlasovou linku, odeslal formular. Z toho vznikne ticket,
ktery ma sveho cloveka a dohledatelny prubeh.
Ticket **neni** bug ani wish. Vyvojarska agenda je jina vec s jinym zivotnim
cyklem a v prototypu zatim neexistuje. Michat je do jedne evidence by znamenalo,
ze ani jedna nefunguje poradne.
## Tri veci, na kterych to stoji
**Kanaly ustuji do ticketu.** WhatsApp, e-mail, hlasova linka a formular jsou
plnohodnotne spoustece. Automatizace zacne prichozi zpravou a skonci zalozenym
ticketem.
**Ticket ma sveho cloveka.** `assignee` neni volny text, ale odkaz na konkretniho
resitele. Da se rict "hod to na Karla Vomacku" a Karel to ma mezi svymi tickety.
Nad tym je prehled pres cely tym, kde je videt, kdo co u sebe ma.
**Kazdy ticket je dohledatelny.** Nese si strom zaznamu o tom, co se s nim delo
a **co ktera sluzba vratila**. Kdyz neco nesedi, neni potreba hadat.
## Datovy model
`src/data/ticketStore.ts`
```ts
interface Ticket {
id: string;
subject: string;
channel: 'whatsapp' | 'email' | 'voice' | 'form' | 'portal';
customer: TicketCustomer;
status: 'new' | 'open' | 'waiting' | 'resolved';
priority: 'low' | 'normal' | 'high' | 'critical';
assignee: { id: string; name: string } | null; // null = ceka ve fronte
automationId: string | null; // null = zalozeno rucne
createdAt: string;
updatedAt: string;
}
interface TicketCustomer {
id: string | null; // null = firmu se nepodarilo dohledat v CRM
company: string;
contact: string;
reply: string; // adresa nebo cislo, kam se odpovida
}
```
`customer.id` je zamerne nullable. Prave na nej se pta podminka "mame zakaznika?"
ve strome automatizace. Bez toho by nesla postavit vetev "firmu neznam, zaloz
obchodni pripad a nekomu to dej".
Uvnitr ulozista se drzi jen `assigneeId`, jmeno se dopocitava pri cteni. Kdyz
resitel ze seznamu zmizi, ticket nespadne - jen se zaloguje a tvari se jako
neprirazeny.
## Resitele
`src/data/people.ts`
Resitel je oddeleny od uzivatele. **Uzivatel** je ten, kdo se prihlasi do portalu,
**resitel** je ten, na koho jde ticket. Casto je to tyz clovek, ale ne vzdy -
technik muze mit tickety a do portalu se nikdy neprihlasit.
Spojka mezi obojim je e-mail. Podle ni funguje filtr "moje tickety"
(`?assignee=me`). Kdyz prihlaseny ucet zadnemu resiteli neodpovida, filtr se
v portalu nabidne jako nedostupny misto toho, aby vracel prazdno bez vysvetleni.
Kazdy resitel ma `capacity`, tedy pocet nevyrizenych ticketu, ktery je pro nej
jeste zdrava zatez. Neni to limit, nic se podle nej neodmita - jen se v prehledu
oznaci, kdo je nad ni.
## Prehled nad firmou
`GET /api/dashboard/tickets/workload` vraci pres cely tym: kolik ma kdo
nevyrizenych, kolik celkem, kolik kritickych, jak stary je jeho nejstarsi ticket
a jestli je nad kapacitu. K tomu pocet ticketu ve fronte bez resitele.
V portalu je to postranni panel na strance Tickety. Radek je zaroven filtr
seznamu - kliknutim na cloveka se seznam zuzi na jeho tickety. Bez toho by to
byl jen obrazek.
Sirka pruhu se pocita proti nejvytizenejsimu clenovi tymu, ne proti kapacite.
Jde o porovnani lidi mezi sebou.
## Log ticketu
Log je **strom**, ne seznam. Vetev podminky visi na zaznamu te podminky, takze je
videt i to, ktera cast behu se vubec nespustila.
```ts
interface TicketTraceEntry {
id: string;
parentId: string | null; // null = hlavni sekvence
kind: 'trigger' | 'action' | 'condition' | 'note';
connectorId: string | null; // ktera sluzba to byla
operationId: string | null;
label: string;
status: 'ok' | 'error' | 'skipped' | 'info';
response: string | null; // co sluzba vratila
durationMs: number | null;
at: string;
}
```
`response` je duvod, proc log existuje. V portalu se zobrazuje rovnou, ne po
rozkliknuti - kvuli nemu se do logu chodi.
Zapisuje se zanorene (`TraceInput` s `children`) a uklada zplostele s `parentId`.
Klient si strom zase poskladá. Zaznam s neznamym rodicem se nezahodi, prida se
do korene a zaloguje - ztratit radek logu je horsi nez ho ukazat spatne zanoreny.
Komentare jsou taky zaznamy logu (`kind: 'note'`). Diky tomu je vsechno na jedne
casove ose a nemusi se nikde skladat dohromady dva ruzne seznamy.
## Konektor Tickety
Kategorie `servicedesk`, driv byl pod Nastroji. Ma obe strany:
| Spoustec | Kdy |
| ------------------ | ------------------------------------------------------ |
| `created` | zalozen ticket, at uz z kanalu nebo rucne |
| `unknown-customer` | k ticketu se nepodarilo dohledat firmu |
| `assigned` | ticket dostal konkretniho cloveka |
| `status-changed` | prechod do jineho stavu vcetne vyreseni |
| Akce | Co dela |
| --------------- | --------------------------------------------- |
| `create` | zalozi pozadavek |
| `assign` | preda ticket cloveku |
| `set-status` | posune stav |
| `link-customer` | doplni firmu z CRM |
| `comment` | zapise komentar do logu |
Typicky retez, ktery z toho jde postavit:
```
Prijata zprava z WhatsApp
Zaradit do kategorie (AI)
Dohledat firmu podle telefonu (CRM)
knownCustomer?
ANO Zalozit ticket, Priradit resiteli
NE Zalozit ticket, Zalozit obchodni pripad, Upozornit servicedesk
```
## Parametry od sluzby
Nektere spoustece si data urcuji samy. E-mail posila `from`, `subject`, `body`.
WhatsApp posila `phone`, `text`. Ticket posila `knownCustomer`. Uzivatel si je
nevymysli, ale potrebuje nad nimi stavet podminky.
Katalog je proto deklaruje v `providedFields` u operace. Chovaji se pak takhle:
- builder je ukazuje **jen ke cteni**, pridat ani prejmenovat nejdou,
- server je pri ulozeni stromu **vzdy dosadi z katalogu** a to, co poslal klient,
zahodi (`normalizeTriggerFields` v `src/routes/dashboard.ts`),
- dosazeni probiha **pred validaci**, jinak by podminky odkazujici na katalogova
ID vypadaly jako rozbite.
ID techto parametru (`email.from`, `ticket.knownCustomer`) musi zustat stabilni.
Odkazuji se na ne podminky v ulozenych stromech, prejmenovani ID je rozbije.
Spoustece bez `providedFields` (webhook, formular) funguji jako predtim -
parametry si deklaruje uzivatel.
## Stranky portalu
```
/dashboard/tickety seznam, filtry, prehled vytizeni
/dashboard/tickety/:id detail: prubeh a log, resitel, zakaznik, puvod
```
## Co chybi
| Chybi | Poznamka |
| ---------------------------- | ------------------------------------------------------- |
| Bugs a wishes | vyvojarska agenda, samostatna evidence |
| Skutecny beh automatizaci | log ticketu zatim plni simulace, ne runtime |
| Napojeni logu na beh | `automationId` je odkaz, historie behu ale neexistuje |
| Odpoved zakaznikovi z detailu| akce `send` u kanalu se z portalu nevola |
| SLA a eskalace | zadne lhuty, `capacity` je jen orientacni |
| Databaze | data v pameti, restart je vrati na vychozi sadu |
+42
View File
@@ -2,6 +2,48 @@
Nejnovejsi nahore.
## 2026-08-03
Tickety predelane na plnohodnotny konektor. Prestavaji byt polozkou v seznamu
a stavaji se prichozim pozadavkem, ktery ma sveho cloveka a dohledatelny prubeh.
Popis modelu je v [06-tickety.md](06-tickety.md).
### Pridano
- Kanaly do ticketu: WhatsApp jako novy konektor, e-mail a hlasova linka
jako plnohodnotne spoustece.
- Konektor Tickety presunut do nove kategorie `servicedesk`, rozsiren
o spoustece `created`, `unknown-customer`, `assigned`, `status-changed`
a akce `assign`, `set-status`, `link-customer`.
- Resitele (`src/data/people.ts`) oddelene od uzivatelu portalu, spojka e-mailem.
- Log ticketu ve strome vcetne toho, co ktera volana sluzba vratila.
- Prehled vytizeni tymu, kdo co ma u sebe, zaroven jako filtr seznamu.
- Detail ticketu `/dashboard/tickety/:id`: prubeh a log, prirazeni, stav,
zakaznik, komentare.
- Filtry seznamu ticketu na serveru: resitel (vcetne `me` a `unassigned`),
stav, kanal.
- `providedFields` v katalogu konektoru: parametry, ktere spoustec predava sam.
- Udalost `ticket.assigned` na sbernici i v portalu.
- Endpointy `/api/dashboard/people`, `/tickets/workload`, `/tickets/:id`
a POST varianty pro assign, status a comment. Vse ve Swaggeru.
### Zmeneno
- `Ticket` ma misto volneho `requester` strukturovaneho `customer` s nullable
`id` firmy v CRM, k tomu `channel`, `assignee` jako odkaz na cloveka
a `automationId`.
- `customer.id === null` je nosna informace, ne chybejici udaj. Prave na ni se
pta podminka "mame zakaznika?" ve strome automatizace.
- Simulace ticketu bere kanal a prepinac, jestli se zakaznik dohleda.
Zakladany ticket dostane cely realisticky log.
- Builder ukazuje parametry od sluzby jen ke cteni. Server je pri ulozeni
vzdy dosadi z katalogu, a to jeste pred validaci stromu.
### Vedome neudelano
Bugs a wishes zustavaji mimo. Vyvojarska agenda ma jiny zivotni cyklus a slucovat
ji s tickety by znamenalo, ze ani jedna evidence nefunguje poradne.
## 2026-07-31
Prvni nasazeni aplikace do repozitare csbot-prototype.