Fronta a worker: webhook odpovi hned, praci udelaji workeri

Webhook uz nic nevykonava v requestu. Zapise udalost do fronty a odpovi 202
do jednotek milisekund; strom vykona worker na pozadi. Za konektory nerucime,
takze cekat na cizi sluzbu v requestu znamena ztracet udalosti pri timeoutu.

Fronta ma opakovani s rostouci prodlevou (30 s, 2 min, 10 min, hodina),
spravedlive poradi po firmach (jedna firma s tisicem udalosti nezablokuje
ostatni), navrat zaseknutych behu po restartu a uklid hotovych. Marna chyba
se neopakuje - chybejici skript za minutu existovat nezacne.

Tri druhy spoustecu: push (webhook), vnitrni udalost (vznik a zmena ticketu)
a pull, tedy pravidelne dotazovani u sluzeb bez webhooku (posta, zpravy).
Planovac jen rekne "je cas", samotny dotaz je prvni krok stromu, takze ma
zaznam v logu a opakuje se pri chybe jako cokoliv jineho.

Kontrakt tela webhooku: kazdy parametr ma cestu (data.order.id,
errors.0.message), takze jde napojit i odesilatel s vnorenym modelem.
U adresy je metoda, ukazka tela a kopiruje se cela adresa vcetne domeny.

Vnitrni kroky, ktere sahaji do naseho uloziste: ticket/upsert (zaloz nebo
dopln podle externiho ID), assign-least-busy, assign-by-external, set-type,
set-stage, add-tags, set-status, incident/create, flow/pause a flow/log.

Faze ticketu jako treti osa vedle stavu a stitku. Stav je zivotni cyklus
a pocitaji se z nej statistiky, faze je workflow daneho typu a muze byt jen
jedna, takze se na ni da spolehnout v podmince.

ID z cizich aplikaci u resitele: voicebot posle voicebotId a ticket skonci
u toho, komu patri. Vazba je na jednom miste, ne v kazde automatizaci.

Kazda chyba zaklada incident se dvema urovnemi: impact cte klient a je
srozumitelny, detail cte admin a je v nem cely beh, ktery krok selhal, cele
hlaseni a data na vstupu. Detail vidi jen spravce platformy.

Ochrana proti smycce: automatizace navazana na zmenu ticketu ticket meni,
cimz se spousti znovu - pri vyvoji to server polozilo. Resi to oznaceni behu
pres AsyncLocalStorage a strop peti behu na jeden ticket za minutu.

Upozorneni pri prideleni prace vcetne cisla u zalozky Tickety. Zivy dashboard:
dlazdice nad nasimi daty na udalost, data z konektoru podle ttlSec s moznosti
vynutit nacteni znovu.

Opraveno: path a intervalSec u spoustece se pri ulozeni zahazovaly; nad
seznamem neslo pouzit contains, takze na stitky neslo postavit podminku;
novejsi vystup kroku ted prekryje starsi misto hlaseni konfliktu.

Overeno dvema scenari proti bezicimu serveru, 34 kontrol: firma se skladem,
expedici a IT, a hovory z voicebota (callSid do externiho ID, status do faze,
prirazeni podle voicebotId, tri zpravy = jeden ticket se tremi udalostmi).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-13 16:41:02 +02:00
co-authored by Claude Opus 5
parent 5d186dcd2e
commit a57eca123e
38 changed files with 2942 additions and 112 deletions
+172
View File
@@ -1968,6 +1968,178 @@ export function buildOpenApiDocument() {
},
},
},
'/api/dashboard/notifications': {
get: {
tags: ['Dashboard'],
summary: 'Upozorneni prihlaseneho',
description:
'Cislo u zalozky Tickety a hlasky o pridelene praci. Upozorneni jsou ulozena, ' +
'takze je najde i ten, kdo mel portal zavreny.',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Upozorneni',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
items: { type: 'array', items: { type: 'object' } },
unread: { type: 'integer' },
mine: { type: 'integer', description: 'Kolik ticketu ma volajici u sebe.' },
},
},
},
},
},
},
},
},
'/api/dashboard/notifications/read': {
post: {
tags: ['Dashboard'],
summary: 'Oznacit upozorneni jako prectena',
security: [{ bearerAuth: [] }],
requestBody: {
required: false,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
ids: {
type: 'array',
items: { type: 'string' },
description: 'Bez seznamu se oznaci vsechna.',
},
},
},
},
},
},
responses: { '200': { description: 'Oznaceno' } },
},
},
'/api/dashboard/runs': {
get: {
tags: ['Automatizace'],
summary: 'Stav fronty behu',
description:
'Kdyz neco nefunguje, tohle je prvni misto, kam se clovek podiva: ceka fronta, ' +
'nebo uz to nekolikrat selhalo? U kazdeho behu je cele chybove hlaseni.',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Fronta a posledni behy',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
stats: {
type: 'object',
properties: {
pending: { type: 'integer' },
running: { type: 'integer' },
done: { type: 'integer' },
failed: { type: 'integer' },
oldestPendingAt: { type: 'string', nullable: true },
},
},
items: { type: 'array', items: { type: 'object' } },
},
},
},
},
},
},
},
},
'/webhook/ticket/{token}': {
post: {
tags: ['Webhook'],
summary: 'Prijem udalosti do ticketu',
description:
'VEREJNY endpoint, autorizuje token firmy v adrese. Se stejnym externalId se ' +
'udalost navesi na existujici ticket, jinak vznikne novy. externalId je ' +
'unikatni v ramci firmy.',
parameters: [{ name: 'token', in: 'path', required: true, schema: { type: 'string' } }],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
externalId: { oneOf: [{ type: 'string' }, { type: 'number' }] },
source: { type: 'string', example: 'eshop' },
event: { type: 'string', example: 'order.created' },
subject: { type: 'string' },
typeId: { type: 'string' },
tags: { type: 'array', items: { type: 'string' } },
fields: { type: 'object', additionalProperties: true },
},
},
},
},
},
responses: {
'201': { description: 'Ticket vznikl' },
'200': { description: 'Udalost se navesila na existujici ticket' },
'400': { description: 'Neplatna data' },
'404': { description: 'Neznamy token' },
},
},
},
'/webhook/{token}': {
post: {
tags: ['Webhook'],
summary: 'Prijem dat do automatizace',
description:
'VEREJNY endpoint. **Odpovi hned** (202) a strom vykona worker na pozadi - ' +
'cizi sluzba muze odpovidat pomalu a odesilateli by vyprsel timeout. ' +
'Telo se kontroluje proti kontraktu spoustece, vcetne vnorenych cest.',
parameters: [{ name: 'token', in: 'path', required: true, schema: { type: 'string' } }],
requestBody: {
required: true,
content: {
'application/json': {
schema: { type: 'object', additionalProperties: true },
},
},
},
responses: {
'202': {
description: 'Prijato, zpracuje se na pozadi',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
accepted: { type: 'boolean' },
automationId: { type: 'string' },
runId: { type: 'string', nullable: true },
},
},
},
},
},
'400': { description: 'Telo neodpovida kontraktu spoustece' },
'404': { description: 'Neznamy token' },
'409': { description: 'Automatizace je pozastavena' },
},
},
get: {
tags: ['Webhook'],
summary: 'Napoveda: co se v tele ceka',
description: 'Vraci metodu, seznam parametru vcetne cest a ukazku tela.',
parameters: [{ name: 'token', in: 'path', required: true, schema: { type: 'string' } }],
responses: {
'200': { description: 'Kontrakt' },
'404': { description: 'Neznamy token' },
},
},
},
'/api/contact': {
post: {
tags: ['Kontakt'],