import { config } from './config.js'; /** * OpenAPI popis API. * * `servers` MUSI obsahovat prefix reverse proxy, jinak Swagger "Try it out" * vola endpointy na root domene a dostane 404 (viz AGENTS.md). * Prefix se bere z ROOT_PATH, nikdy se nehardcoduje. */ export function buildOpenApiDocument() { const server = config.rootPath === '' ? '/' : config.rootPath; return { openapi: '3.0.3', info: { title: 'Automia - portal a API', version: '1.0.0', description: 'Webova prezentace a klientsky portal. Automatizace, voiceboti, integrace, ' + 'tickety a incidenty. Aplikace bezi za reverse proxy AppFactory.', }, servers: [{ url: server, description: 'Verejna adresa vcetne prefixu proxy' }], tags: [ { name: 'Provoz', description: 'Health a zakladni informace' }, { name: 'Autentizace', description: 'Prihlaseni do portalu' }, { name: 'Dashboard', description: 'Data klientskeho portalu' }, { name: 'Tickety', description: 'Pozadavky, jejich resitele a log prubehu' }, { name: 'Automatizace', description: 'Sprava automatizaci a stromu akci' }, { name: 'Sluzby', description: 'Katalog toho, co umime napojit' }, { name: 'Konektory', description: 'Napojeni firmy na sluzbu vcetne pristupovych udaju' }, { name: 'Skripty', description: 'Vykonna cast sluzby: manifest, kod a zkusebni beh' }, { name: 'Simulace', description: 'Vyvolani provoznich udalosti pro nahled' }, { name: 'Webhook', description: 'Verejny prijem dat do automatizace' }, { name: 'Kontakt', description: 'Poptavkovy formular z webu' }, ], components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', description: 'Token z POST /api/auth/login. Vlozte samotny token bez slova Bearer.', }, }, schemas: { Error: { type: 'object', properties: { error: { type: 'string', example: 'validation_error' }, message: { type: 'string', example: 'Zadejte platny e-mail.' }, }, }, User: { type: 'object', description: 'Uzivatel muze patrit do vic firem. Role je vzdy az uvnitr firmy, ' + 'pristup napric firmami je zvlast jako platformAdmin.', properties: { id: { type: 'string', example: 'usr_1' }, email: { type: 'string', example: 'admin@automia.cz' }, name: { type: 'string', example: 'Jiri Uhlir' }, platformAdmin: { type: 'boolean', description: 'Vidi napric vsemi firmami a muze mezi nimi prepinat.', }, memberships: { type: 'array', items: { type: 'object', properties: { tenantId: { type: 'string', example: 'tnt_automia' }, role: { type: 'string', enum: ['admin', 'agent'] }, }, }, }, }, }, Access: { type: 'object', description: 'Co uzivatel smi. Klient podle toho kresli prepinac pohledu.', properties: { scopes: { type: 'array', items: { type: 'string', enum: ['all', 'tenant', 'mine'] }, }, tenants: { type: 'array', items: { type: 'object', properties: { id: { type: 'string', example: 'tnt_automia' }, name: { type: 'string', example: 'Automia' }, }, }, }, defaultTenantId: { type: 'string', nullable: true }, canAssignOthers: { type: 'boolean', description: 'Smi prehazovat tickety mezi lidmi, ne jen brat na sebe.', }, personId: { type: 'string', nullable: true }, }, }, Connector: { type: 'object', description: 'Napojeni firmy na jednu sluzbu. Hodnoty pristupovych udaju tady zamerne ' + 'nejsou a nikdy nebudou - secrets se z beznych endpointu nevraci.', properties: { id: { type: 'string', example: 'con_1a2b3c4d' }, tenantId: { type: 'string', example: 'tnt_automia' }, serviceId: { type: 'string', example: 'idoklad' }, name: { type: 'string', example: 'iDoklad Automia' }, baseUrl: { type: 'string', nullable: true }, enabled: { type: 'boolean' }, status: { type: 'string', enum: ['untested', 'ok', 'error'] }, lastCheckAt: { type: 'string', format: 'date-time', nullable: true }, lastError: { type: 'string', nullable: true }, isDefault: { type: 'boolean', description: 'Krok stromu bez vybraneho konektoru pouzije tenhle.', }, filled: { type: 'array', items: { type: 'string' }, description: 'ID poli, ktera jsou vyplnena. Hodnoty se nevraci.', }, missing: { type: 'array', items: { type: 'string' }, description: 'ID povinnych poli, ktera chybi.', }, config: { type: 'object', additionalProperties: { type: 'string' }, description: 'Necitliva nastaveni. Tajna pole tu nejsou vubec.', }, ready: { type: 'boolean' }, }, }, ScriptField: { type: 'object', description: 'Parametr skriptu. Stejny tvar pro vstup i vystup - kontrola je pak ' + 'jedna funkce, ne dve skoro stejne.', required: ['id', 'label', 'type', 'required'], properties: { id: { type: 'string', example: 'invoiceId', description: 'Pouziva se v sablonach jako {{invoiceId}}.', }, label: { type: 'string', example: 'ID faktury v iDokladu' }, type: { type: 'string', enum: ['string', 'number', 'boolean', 'date'] }, required: { type: 'boolean' }, hint: { type: 'string' }, options: { type: 'array', description: 'Vyber z hodnot. Jina hodnota neprojde kontrolou.', items: { type: 'object', properties: { value: { type: 'string' }, label: { type: 'string' } }, }, }, pattern: { type: 'string', description: 'Jen u typu string.' }, multiline: { type: 'boolean', description: 'Jen u typu string.' }, default: { description: 'Dosadi se, kdyz hodnota chybi a parametr neni povinny.' }, }, }, ScriptManifest: { type: 'object', description: 'Co skript umi. Podle nej s nim umi pracovat strom automatizace.', properties: { id: { type: 'string', example: 'idoklad.get-issued-invoice', description: 'Tvar sluzba.operace. Nazev souboru musi byt .js.', }, serviceId: { type: 'string', example: 'idoklad' }, serviceName: { type: 'string', example: 'iDoklad' }, operationId: { type: 'string', example: 'get-issued-invoice' }, name: { type: 'string', example: 'Získat vydanou fakturu' }, description: { type: 'string' }, inputs: { type: 'array', items: { $ref: '#/components/schemas/ScriptField' } }, outputs: { type: 'array', items: { $ref: '#/components/schemas/ScriptField' } }, timeoutMs: { type: 'integer', example: 15000 }, }, }, ScriptProblem: { type: 'object', description: 'Rozbity skript. Nesmi shodit ostatni ani tise zmizet, proto se vraci sem.', properties: { file: { type: 'string', example: 'idoklad.get-issued-invoice.js' }, scriptId: { type: 'string', nullable: true }, message: { type: 'string' }, issues: { type: 'array', items: { type: 'object', properties: { field: { type: 'string' }, message: { type: 'string' } }, }, }, }, }, ConnectionStatus: { type: 'object', description: 'Stav napojeni konektoru. Hodnoty pristupovych udaju se nevraci nikdy, ' + 'jen jmena promennych, ktere chybi.', properties: { connectorId: { type: 'string', example: 'idoklad' }, baseUrl: { type: 'string', example: 'https://services.csbot.cz/apps/idoklad' }, ready: { type: 'boolean' }, missing: { type: 'array', items: { type: 'string' }, example: ['IDOKLAD_CLIENT_SECRET'], }, headers: { type: 'array', items: { type: 'string' }, example: ['X-ClientId'] }, }, }, ScriptRunResult: { type: 'object', description: 'Vysledek behu skriptu. `retryable` rika, jestli ma smysl zkusit to znovu - ' + 'timeout ano, spatny vstup ne.', properties: { ok: { type: 'boolean' }, scriptId: { type: 'string' }, outputs: { type: 'object', additionalProperties: true, description: 'Prazdne, kdyz beh selhal.', }, logs: { type: 'array', items: { type: 'object', properties: { at: { type: 'string', format: 'date-time' }, message: { type: 'string' }, detail: { type: 'string' }, }, }, }, durationMs: { type: 'integer' }, httpCalls: { type: 'integer' }, error: { type: 'object', nullable: true, properties: { kind: { type: 'string', enum: [ 'not_found', 'config', 'validation', 'output', 'retryable', 'terminal', 'timeout', 'internal', ], }, message: { type: 'string' }, retryable: { type: 'boolean' }, status: { type: 'integer' }, detail: { type: 'string' }, issues: { type: 'array', items: { type: 'object', properties: { field: { type: 'string' }, message: { type: 'string' } }, }, }, }, }, }, }, LoginRequest: { type: 'object', required: ['email', 'password'], properties: { email: { type: 'string', format: 'email', example: 'admin@automia.cz' }, password: { type: 'string', format: 'password', example: 'demo1234' }, }, }, LoginResponse: { type: 'object', properties: { token: { type: 'string' }, user: { $ref: '#/components/schemas/User' }, }, }, Person: { type: 'object', description: 'Resitel ticketu. Nemusi mit ucet v portalu, spojka je e-mail.', properties: { id: { type: 'string', example: 'ppl_vomacka' }, name: { type: 'string', example: 'Karel Vomacka' }, email: { type: 'string', format: 'email' }, role: { type: 'string', example: 'Servicedesk' }, capacity: { type: 'integer', description: 'Kolik nevyrizenych ticketu je pro nej jeste zdrava zatez.', }, }, }, TicketCustomer: { type: 'object', properties: { id: { type: 'string', nullable: true, description: 'ID firmy v CRM. null = zakaznika se nepodarilo dohledat.', example: 'crm_1042', }, company: { type: 'string', example: 'Firma s.r.o.' }, contact: { type: 'string', example: 'Petra Klientova' }, reply: { type: 'string', description: 'Adresa nebo cislo, odkud pozadavek prisel a kam se odpovida.', }, }, }, Ticket: { type: 'object', properties: { id: { type: 'string', example: 'TK-4821' }, subject: { type: 'string' }, body: { type: 'string', description: 'Cely text pozadavku. Prazdny retezec = krok "Zalozit ticket" obsah nenaplnil.', }, sourceRef: { type: 'string', nullable: true, description: 'Odkaz na zdrojovou zpravu u poskytovatele.', example: 'wamid.HBgLNDIwNzc0OTAyMzMx', }, channel: { type: 'string', enum: ['whatsapp', 'facebook', 'instagram', 'email', 'voice', 'form', 'portal'], description: 'Odkud pozadavek prisel.', }, customer: { $ref: '#/components/schemas/TicketCustomer' }, status: { type: 'string', enum: ['new', 'open', 'waiting', 'resolved'] }, priority: { type: 'string', enum: ['low', 'normal', 'high', 'critical'] }, assignee: { type: 'object', nullable: true, description: 'Kdo ma ticket u sebe. null = ceka ve fronte.', properties: { id: { type: 'string', example: 'ppl_vomacka' }, name: { type: 'string', example: 'Karel Vomacka' }, }, }, automationId: { type: 'string', nullable: true, description: 'Automatizace, ktera ticket zalozila. null = zalozeno rucne.', }, createdAt: { type: 'string', format: 'date-time' }, updatedAt: { type: 'string', format: 'date-time' }, }, }, TicketTraceEntry: { type: 'object', description: 'Jeden radek logu ticketu. Strom se sklada pres parentId - vetev podminky ' + 'visi na zaznamu te podminky.', properties: { id: { type: 'string', example: 'tr_12' }, parentId: { type: 'string', nullable: true, description: 'null = zaznam v hlavni sekvenci.', }, kind: { type: 'string', enum: ['trigger', 'action', 'condition', 'note'] }, connectorId: { type: 'string', nullable: true, example: 'raynet' }, operationId: { type: 'string', nullable: true, example: 'upsert-contact' }, label: { type: 'string' }, status: { type: 'string', enum: ['ok', 'error', 'skipped', 'info'] }, response: { type: 'string', nullable: true, description: 'Co sluzba vratila. Kvuli tomuhle log existuje.', }, durationMs: { type: 'integer', nullable: true }, at: { type: 'string', format: 'date-time' }, }, }, TicketDetail: { allOf: [ { $ref: '#/components/schemas/Ticket' }, { type: 'object', properties: { trace: { type: 'array', items: { $ref: '#/components/schemas/TicketTraceEntry' }, }, }, }, ], }, Workload: { type: 'object', description: 'Prehled nad firmou - kdo ma kolik ticketu u sebe.', properties: { rows: { type: 'array', items: { type: 'object', properties: { person: { $ref: '#/components/schemas/Person' }, open: { type: 'integer', description: 'Nevyresene tickety.' }, total: { type: 'integer' }, critical: { type: 'integer' }, oldestOpenAt: { type: 'string', format: 'date-time', nullable: true }, overloaded: { type: 'boolean' }, }, }, }, unassigned: { type: 'integer', description: 'Nevyresene tickety bez resitele.' }, openTotal: { type: 'integer' }, }, }, Incident: { type: 'object', properties: { id: { type: 'string', example: 'INC-231' }, title: { type: 'string' }, service: { type: 'string' }, severity: { type: 'string', enum: ['sev1', 'sev2', 'sev3'] }, status: { type: 'string', enum: ['investigating', 'identified', 'monitoring', 'resolved'], }, startedAt: { type: 'string', format: 'date-time' }, resolvedAt: { type: 'string', format: 'date-time', nullable: true }, }, }, TriggerField: { type: 'object', required: ['id', 'name', 'type', 'required'], properties: { id: { type: 'string', example: 'f_42' }, name: { type: 'string', example: 'score', description: 'Klic v prichozich datech, pismena, cislice a podtrzitko.', }, type: { type: 'string', enum: ['string', 'number', 'boolean', 'date'] }, required: { type: 'boolean' }, }, }, FlowStep: { type: 'object', description: 'Krok stromu. Bud akce nad konektorem, nebo podminka se dvema vetvemi.', properties: { id: { type: 'string' }, kind: { type: 'string', enum: ['action', 'condition'] }, connectorId: { type: 'string', example: 'email' }, operationId: { type: 'string', example: 'send' }, inputs: { type: 'object', additionalProperties: { type: 'string' }, description: 'Nastaveni akce. Klic je ID pole z katalogu, hodnota je sablona - ' + '{{nazev}} se nahradi parametrem spoustece. Neznamy klic vraci 400.', example: { subject: 'Reklamace od {{profileName}}', body: '{{text}}' }, }, fieldId: { type: 'string', example: 'f_42' }, operator: { type: 'string', enum: [ 'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'startsWith', 'isEmpty', 'isNotEmpty', 'isTrue', 'isFalse', ], }, value: { type: 'string', example: '15' }, yes: { type: 'array', items: { $ref: '#/components/schemas/FlowStep' } }, no: { type: 'array', items: { $ref: '#/components/schemas/FlowStep' } }, }, }, AutomationFlow: { type: 'object', properties: { trigger: { type: 'object', nullable: true, properties: { connectorId: { type: 'string', example: 'webhook' }, operationId: { type: 'string', example: 'received' }, fields: { type: 'array', items: { $ref: '#/components/schemas/TriggerField' }, }, webhookToken: { type: 'string', readOnly: true, description: 'Generuje vyhradne server, hodnota od klienta se ignoruje.', }, }, }, steps: { type: 'array', items: { $ref: '#/components/schemas/FlowStep' } }, }, }, Automation: { type: 'object', properties: { id: { type: 'string', example: 'AUT-01' }, name: { type: 'string' }, kind: { type: 'string', enum: ['workflow', 'voicebot', 'integrace', 'report'] }, enabled: { type: 'boolean' }, runsToday: { type: 'integer' }, successRate: { type: 'number' }, avgDurationMs: { type: 'integer' }, lastRunAt: { type: 'string', format: 'date-time' }, stepCount: { type: 'integer' }, configured: { type: 'boolean' }, issues: { type: 'array', items: { type: 'string' }, description: 'Co chybi k zapnuti. Prazdne pole znamena hotovo.', }, }, }, AutomationDetail: { allOf: [ { $ref: '#/components/schemas/Automation' }, { type: 'object', properties: { flow: { $ref: '#/components/schemas/AutomationFlow' }, createdAt: { type: 'string', format: 'date-time' }, updatedAt: { type: 'string', format: 'date-time' }, }, }, ], }, }, }, paths: { '/health': { get: { tags: ['Provoz'], summary: 'Health check', description: 'Vraci 200, pokud je aplikace schopna prijimat provoz.', responses: { '200': { description: 'Aplikace bezi', content: { 'application/json': { schema: { type: 'object', properties: { status: { type: 'string', example: 'ok' }, uptimeSec: { type: 'integer', example: 42 }, }, }, }, }, }, }, }, }, '/api/auth/login': { post: { tags: ['Autentizace'], summary: 'Prihlaseni', requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/LoginRequest' } }, }, }, responses: { '200': { description: 'Token a udaje uzivatele', content: { 'application/json': { schema: { $ref: '#/components/schemas/LoginResponse' } }, }, }, '401': { description: 'Nespravny e-mail nebo heslo', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, }, }, }, '/api/auth/me': { get: { tags: ['Autentizace'], summary: 'Prihlaseny uzivatel', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Udaje uzivatele', content: { 'application/json': { schema: { type: 'object', properties: { user: { $ref: '#/components/schemas/User' } }, }, }, }, }, '401': { description: 'Chybi nebo neplatny token' }, }, }, }, '/api/auth/logout': { post: { tags: ['Autentizace'], summary: 'Odhlaseni', security: [{ bearerAuth: [] }], responses: { '204': { description: 'Odhlaseno' } }, }, }, '/api/dashboard/summary': { get: { tags: ['Dashboard'], summary: 'Souhrn pro prehled', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Souhrnne metriky a casova rada' } }, }, }, '/api/dashboard/widgets': { get: { tags: ['Dashboard'], summary: 'Katalog widgetu prehledu', description: 'Co jde polozit na dashboard vcetne povolenych sirek.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Widgety' } }, }, }, '/api/dashboard/layout': { get: { tags: ['Dashboard'], summary: 'Rozlozeni dashboardu', description: 'Uklada se pro dvojici uzivatel a firma. `custom: false` znamena, ' + 'ze uzivatel kouka na vychozi rozlozeni.', security: [{ bearerAuth: [] }], parameters: [ { name: 'tenantId', in: 'query', schema: { type: 'string' }, description: 'Firma. Bez ni se pouzije prvni, do ktere uzivatel patri.', }, ], responses: { '200': { description: 'Rozlozeni' }, '403': { description: 'Ucet nepatri do zadne firmy' }, '404': { description: 'Firma neexistuje, nebo do ni uzivatel nepatri' }, }, }, put: { tags: ['Dashboard'], summary: 'Ulozit rozlozeni', description: 'Overuje se proti katalogu. Neznamy widget nebo sirka vraci 400.', security: [{ bearerAuth: [] }], parameters: [{ name: 'tenantId', in: 'query', schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['items'], properties: { items: { type: 'array', items: { type: 'object', required: ['id', 'widgetId', 'size'], properties: { id: { type: 'string', example: 'w1' }, widgetId: { type: 'string', example: 'stat.openTickets' }, size: { type: 'string', enum: ['third', 'half', 'full'] }, }, }, }, }, }, }, }, }, responses: { '200': { description: 'Ulozeno' }, '400': { description: 'Neplatne rozlozeni' }, }, }, delete: { tags: ['Dashboard'], summary: 'Vratit na vychozi rozlozeni', security: [{ bearerAuth: [] }], parameters: [{ name: 'tenantId', in: 'query', schema: { type: 'string' } }], responses: { '200': { description: 'Vychozi rozlozeni' } }, }, }, '/api/dashboard/access': { get: { tags: ['Dashboard'], summary: 'Co uzivatel smi videt', description: 'Povolene pohledy, firmy k prepinani a prava. Klient si to nesmi dovozovat sam.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Opravneni', content: { 'application/json': { schema: { $ref: '#/components/schemas/Access' } }, }, }, }, }, }, '/api/dashboard/people': { get: { tags: ['Tickety'], summary: 'Seznam resitelu', description: 'Lide, na ktere jde ticket priradit. `meId` je resitel odpovidajici ' + 'prihlasenemu uzivateli, nebo null, pokud zadny neni.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Resitele', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/Person' } }, meId: { type: 'string', nullable: true }, }, }, }, }, }, }, }, }, '/api/dashboard/tickets': { get: { tags: ['Tickety'], summary: 'Seznam ticketu', description: 'Neznama hodnota filtru se ignoruje a zaloguje, seznam se nezuzi.', security: [{ bearerAuth: [] }], parameters: [ { name: 'scope', in: 'query', schema: { type: 'string', enum: ['all', 'tenant', 'mine'] }, description: '`all` napric firmami (jen platformni admin), `tenant` cela firma, ' + '`mine` jen moje tickety. Nepovoleny pohled vraci 403, nikdy se tise nezuzi.', }, { name: 'tenantId', in: 'query', schema: { type: 'string' }, description: 'Firma u pohledu `tenant` a `mine`. Bez clenstvi vraci 404.', example: 'tnt_automia', }, { name: 'assignee', in: 'query', schema: { type: 'string' }, description: 'ID resitele nebo `unassigned` pro frontu. U pohledu `mine` se ignoruje.', }, { name: 'status', in: 'query', schema: { type: 'string', enum: ['new', 'open', 'waiting', 'resolved'] }, }, { name: 'channel', in: 'query', schema: { type: 'string', enum: ['whatsapp', 'email', 'voice', 'form', 'portal'] }, }, ], responses: { '200': { description: 'Tickety', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/Ticket' } }, meId: { type: 'string', nullable: true }, }, }, }, }, }, }, }, }, '/api/dashboard/tickets/workload': { get: { tags: ['Tickety'], summary: 'Kdo co ma u sebe', description: 'Prehled zateze pres cely tym vcetne poctu ticketu ve fronte.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Vytizeni resitelu', content: { 'application/json': { schema: { $ref: '#/components/schemas/Workload' } }, }, }, }, }, }, '/api/dashboard/tickets/{id}': { get: { tags: ['Tickety'], summary: 'Detail ticketu vcetne logu', description: 'Log obsahuje i to, co ktera volana sluzba vratila.', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'TK-4821', }, ], responses: { '200': { description: 'Detail', content: { 'application/json': { schema: { $ref: '#/components/schemas/TicketDetail' } }, }, }, '404': { description: 'Neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/assign': { post: { tags: ['Tickety'], summary: 'Priradit resitele', description: 'Poslete null pro vraceni ticketu do fronty.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['assigneeId'], properties: { assigneeId: { type: 'string', nullable: true, example: 'ppl_vomacka' }, }, }, }, }, }, responses: { '200': { description: 'Prirazeno', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } }, }, }, '400': { description: 'Chybi assigneeId' }, '404': { description: 'Ticket nebo resitel neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/status': { post: { tags: ['Tickety'], summary: 'Zmenit stav ticketu', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['status'], properties: { status: { type: 'string', enum: ['new', 'open', 'waiting', 'resolved'] }, }, }, }, }, }, responses: { '200': { description: 'Zmeneno', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } }, }, }, '400': { description: 'Neplatny stav' }, '404': { description: 'Neexistuje' }, }, }, }, '/api/dashboard/tickets/{id}/comment': { post: { tags: ['Tickety'], summary: 'Pridat komentar', description: 'Komentar je dalsi radek logu, aby bylo vse na jedne casove ose.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['text'], properties: { text: { type: 'string', minLength: 2 } }, }, }, }, }, responses: { '200': { description: 'Zapsano', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ticket' } }, }, }, '400': { description: 'Prazdny komentar' }, '404': { description: 'Neexistuje' }, }, }, }, '/api/dashboard/incidents': { get: { tags: ['Dashboard'], summary: 'Seznam incidentu', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Incidenty', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/Incident' } }, }, }, }, }, }, }, }, }, '/api/dashboard/stream': { get: { tags: ['Dashboard'], summary: 'Zivy stream zmen (SSE)', description: 'Server-Sent Events. Drzi otevrene spojeni a posila udalosti, jakmile nastanou. ' + 'Swagger UI streamovanou odpoved nezobrazi rozumne, testujte prohlizecem nebo curl.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Proud udalosti text/event-stream' } }, }, }, '/health/ready': { get: { tags: ['Provoz'], summary: 'Readiness vcetne databaze', description: 'Vraci 503, kdyz je databaze nastavena a nedostupna. `/health` na databazi ' + 'zamerne nezavisi - kratky vypadek DB by jinak vedl k restartovani containeru.', responses: { '200': { description: 'Aplikace je pripravena' }, '503': { description: 'Databaze je nastavena, ale nedostupna' }, }, }, }, '/api/dashboard/storage': { get: { tags: ['Dashboard'], summary: 'Kam se uklada', description: 'mode postgres nebo memory. `ephemeral: true` znamena, ze restart procesu ' + 'data smaze. Portal to musi umet rict nahlas.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Rezim uloziste', content: { 'application/json': { schema: { type: 'object', properties: { mode: { type: 'string', enum: ['postgres', 'memory'] }, reason: { type: 'string', nullable: true }, ephemeral: { type: 'boolean' }, }, }, }, }, }, }, }, }, '/api/dashboard/services': { get: { tags: ['Sluzby'], summary: 'Katalog sluzeb pro builder', description: 'Vraci jen sluzby, ktere uzivatel vidi. Neviditelna sluzba v odpovedi neni ' + 'vubec, ne se stavem "nemate pravo". Operace, ktere obsluhuje skript, nesou ' + 'implementation: script a maji skutecne inputs a outputFields.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Sluzby, kategorie a operatory podminek' } }, }, }, '/api/dashboard/connectors/services': { get: { tags: ['Sluzby'], summary: 'Katalog sluzeb ocima firmy', description: 'Jako /services, navic connectorCount, tedy kolik konektoru na sluzbu firma ma. ' + 'Podle toho se rozlisi napojeno od muzete si napojit. Neni to vlastnost sluzby, ' + 'ale te firmy.', security: [{ bearerAuth: [] }], parameters: [{ name: 'tenantId', in: 'query', schema: { type: 'string' } }], responses: { '200': { description: 'Sluzby vcetne pouziti ve firme' } }, }, }, '/api/dashboard/connectors': { get: { tags: ['Konektory'], summary: 'Konektory firmy', description: 'Hodnoty pristupovych udaju se NIKDY nevraci, jen filled (co je vyplnene) ' + 'a missing (ktera povinna pole chybi).', security: [{ bearerAuth: [] }], parameters: [ { name: 'tenantId', in: 'query', schema: { type: 'string' } }, { name: 'serviceId', in: 'query', schema: { type: 'string' } }, ], responses: { '200': { description: 'Konektory firmy', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/Connector' }, }, tenantId: { type: 'string' }, }, }, }, }, }, }, }, post: { tags: ['Konektory'], summary: 'Zalozit konektor', description: 'Pristupove udaje se posilaji ve values s klici podle Service.credentials. ' + 'Nevyplnene povinne pole neni chyba, konektor se ulozi a jen nepujde pouzit.', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['serviceId', 'name'], properties: { serviceId: { type: 'string', example: 'idoklad' }, name: { type: 'string', example: 'iDoklad Celo' }, baseUrl: { type: 'string', nullable: true }, values: { type: 'object', additionalProperties: { type: 'string' }, }, }, }, }, }, }, responses: { '201': { description: 'Zalozeno', content: { 'application/json': { schema: { $ref: '#/components/schemas/Connector' } }, }, }, '400': { description: 'Obecna sluzba konektor nepotrebuje, nebo nezname pole' }, '404': { description: 'Sluzba neexistuje nebo ji uzivatel nevidi' }, }, }, }, '/api/dashboard/connectors/{id}': { get: { tags: ['Konektory'], summary: 'Detail konektoru', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'Konektor' }, '404': { description: 'Neexistuje' } }, }, patch: { tags: ['Konektory'], summary: 'Upravit konektor', description: 'Ve values staci poslat jen to, co se meni. PRAZDNY RETEZEC hodnotu smaze, ' + 'chybejici klic ji nechava - diky tomu jde ulozit formular, ktery tajne hodnoty ' + 'neposila. Zmena udaju vzdy zrusi predchozi overeni.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string' }, baseUrl: { type: 'string', nullable: true }, values: { type: 'object', additionalProperties: { type: 'string' } }, enabled: { type: 'boolean' }, isDefault: { type: 'boolean', enum: [true] }, }, }, }, }, }, responses: { '200': { description: 'Upraveno' }, '404': { description: 'Neexistuje' } }, }, delete: { tags: ['Konektory'], summary: 'Smazat konektor', description: 'Kdyz zmizel vychozi konektor, prevezme to prvni zbyly - jinak by kroky bez ' + 'vybraneho konektoru prestaly fungovat.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { '204': { description: 'Smazano' }, '404': { description: 'Neexistuje' } }, }, }, '/api/dashboard/connectors/{id}/test': { post: { tags: ['Konektory'], summary: 'Overit napojeni', description: 'Zavola verifyPath sluzby, coz je zamerne cteci volani vyzadujici autorizaci. ' + 'Kdyz ho sluzba nema, overi se jen /health a odpoved to v checked rekne - aby ' + 'si nikdo nemyslel, ze jsou overene i pristupove udaje. Neuspesne overeni neni ' + 'chyba API, vraci se 200 s ok: false.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'Vysledek overeni', content: { 'application/json': { schema: { type: 'object', properties: { ok: { type: 'boolean' }, checked: { type: 'string' }, status: { type: 'integer' }, message: { type: 'string' }, }, }, }, }, }, '404': { description: 'Konektor neexistuje' }, }, }, }, '/api/dashboard/scripts': { get: { tags: ['Skripty'], summary: 'Seznam skriptu konektoru', description: 'Manifesty vsech nactenych skriptu, rozbite skripty v `problems` ' + 'a stav napojeni v `connections`. Pristupove udaje se nikdy nevraci, ' + 'jen jmena chybejicich environment variables.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Skripty, problemy a stav napojeni', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/ScriptManifest' }, }, problems: { type: 'array', items: { $ref: '#/components/schemas/ScriptProblem' }, }, connections: { type: 'array', items: { $ref: '#/components/schemas/ConnectionStatus' }, }, directory: { type: 'string', example: '/app/scripts' }, }, }, }, }, }, }, }, }, '/api/dashboard/scripts/reload': { post: { tags: ['Skripty'], summary: 'Znovu nacist skripty ze slozky', description: 'Skripty se nacitaji samy podle casu zmeny souboru. Tenhle endpoint ' + 'to jen vynuti hned, bez cekani.', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Skripty po nacteni' }, '403': { description: 'Jen spravce platformy' }, }, }, }, '/api/dashboard/scripts/{id}': { get: { tags: ['Skripty'], summary: 'Manifest a kod skriptu', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'idoklad.get-issued-invoice', }, ], responses: { '200': { description: 'Kod se vraci vzdy. `manifest` je null, kdyz je skript rozbity.', }, '400': { description: 'Neplatne ID skriptu' }, '404': { description: 'Skript neexistuje' }, }, }, put: { tags: ['Skripty'], summary: 'Ulozit kod skriptu', description: 'Nejdriv se kod nacte a overi, az pak prepise soubor. Rozbita uprava ' + 'se neulozi a puvodni skript dal funguje.', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'idoklad.get-issued-invoice', }, ], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['code'], properties: { code: { type: 'string', description: 'Cely obsah souboru vcetne exportu manifest a run.', }, }, }, }, }, }, responses: { '200': { description: 'Ulozeno, vraci se overeny manifest' }, '400': { description: 'Kod nebo manifest neprosel, v `issues` je co opravit', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } }, }, '403': { description: 'Jen spravce platformy' }, }, }, }, '/api/dashboard/scripts/{id}/test': { post: { tags: ['Skripty'], summary: 'Zkusebni spusteni skriptu', description: 'POZOR: vola opravdovou sluzbu. Vystavena faktura opravdu vznikne. ' + 'Chyba skriptu neni chyba API, vraci se 200 s popisem v `error`.', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'idoklad.get-issued-invoice', }, ], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { inputs: { type: 'object', additionalProperties: true, example: { invoiceId: 12345 }, }, }, }, }, }, }, responses: { '200': { description: 'Vysledek behu', content: { 'application/json': { schema: { $ref: '#/components/schemas/ScriptRunResult' } }, }, }, '400': { description: 'Neplatne ID nebo vstupy' }, '403': { description: 'Jen spravce platformy' }, }, }, }, '/api/dashboard/automations': { get: { tags: ['Automatizace'], summary: 'Seznam automatizaci', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Automatizace', content: { 'application/json': { schema: { type: 'object', properties: { items: { type: 'array', items: { $ref: '#/components/schemas/Automation' } }, }, }, }, }, }, }, }, post: { tags: ['Automatizace'], summary: 'Zalozit automatizaci', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['name'], properties: { name: { type: 'string', minLength: 3 } }, }, }, }, }, responses: { '201': { description: 'Vytvoreno', content: { 'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } }, }, }, '400': { description: 'Neplatny nazev' }, }, }, }, '/api/dashboard/automations/{id}': { parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' }, example: 'AUT-01' }, ], get: { tags: ['Automatizace'], summary: 'Detail vcetne stromu akci', security: [{ bearerAuth: [] }], responses: { '200': { description: 'Detail', content: { 'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } }, }, }, '404': { description: 'Neexistuje' }, }, }, put: { tags: ['Automatizace'], summary: 'Ulozit automatizaci', description: 'Validuje strom proti katalogu konektoru. Nedokoncenou automatizaci server ' + 'nezapne ani pri enabled=true, duvody vraci v poli issues.', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string', minLength: 3 }, enabled: { type: 'boolean' }, flow: { $ref: '#/components/schemas/AutomationFlow' }, }, }, }, }, }, responses: { '200': { description: 'Ulozeno', content: { 'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } }, }, }, '400': { description: 'Neplatny strom' }, '404': { description: 'Neexistuje' }, }, }, delete: { tags: ['Automatizace'], summary: 'Smazat automatizaci', security: [{ bearerAuth: [] }], responses: { '204': { description: 'Smazano' }, '404': { description: 'Neexistuje' } }, }, }, '/api/dashboard/automations/{id}/webhook/regenerate': { post: { tags: ['Automatizace'], summary: 'Nova adresa webhooku', description: 'Stara adresa okamzite prestane fungovat.', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], responses: { '200': { description: 'Novy token', content: { 'application/json': { schema: { $ref: '#/components/schemas/AutomationDetail' } }, }, }, '404': { description: 'Neexistuje nebo spoustecem neni webhook' }, }, }, }, '/api/simulate': { post: { tags: ['Simulace'], summary: 'Vyvolat provozni udalost', description: 'Meni skutecna data, aby bylo videt, jak dashboard reaguje zive. ' + 'Nevyplnena pole server doplni ukazkovou hodnotou.', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['action'], properties: { action: { type: 'string', enum: [ 'ticket.created', 'ticket.resolved', 'incident.started', 'incident.resolved', 'automation.run', ], }, subject: { type: 'string' }, body: { type: 'string', description: 'Cely text pozadavku.' }, channel: { type: 'string', enum: ['whatsapp', 'facebook', 'instagram', 'email', 'voice', 'form', 'portal'], description: 'Odkud pozadavek prisel. Podle toho se poskladá i log ticketu.', }, contact: { type: 'string' }, knownCustomer: { type: 'boolean', description: 'false = CRM firmu nedohleda, ticket zustane bez zakaznika i bez resitele.', }, priority: { type: 'string', enum: ['low', 'normal', 'high', 'critical'] }, ticketId: { type: 'string' }, title: { type: 'string' }, service: { type: 'string' }, severity: { type: 'string', enum: ['sev1', 'sev2', 'sev3'] }, incidentId: { type: 'string' }, automationId: { type: 'string' }, ok: { type: 'boolean' }, }, }, }, }, }, responses: { '200': { description: 'Udalost provedena' }, '201': { description: 'Zaznam vytvoren' }, '409': { description: 'Neni co provest' }, }, }, }, '/webhook/{token}': { post: { tags: ['Webhook'], summary: 'Prijem dat do automatizace', description: 'Verejny endpoint bez prihlaseni. Autorizuje neuhodnutelny token v adrese. ' + 'Telo se overuje proti parametrum deklarovanym u spoustece.', parameters: [ { name: 'token', in: 'path', required: true, schema: { type: 'string' }, description: '32znakovy token vygenerovany serverem.', }, ], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', additionalProperties: true }, example: { customer: 'Nordis', score: 18, comment: 'vse ok' }, }, }, }, responses: { '202': { description: 'Prijato' }, '400': { description: 'Chybi parametr nebo nesedi typ' }, '404': { description: 'Neznamy token' }, '409': { description: 'Automatizace je pozastavena' }, }, }, }, '/api/contact': { post: { tags: ['Kontakt'], summary: 'Odeslat poptavku z webu', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['name', 'email', 'topic', 'message'], properties: { name: { type: 'string', minLength: 2 }, email: { type: 'string', format: 'email' }, company: { type: 'string' }, phone: { type: 'string' }, topic: { type: 'string', enum: [ 'automatizace', 'voicebot', 'integrace', 'dashboard', 'podpora', 'jine', ], }, message: { type: 'string', minLength: 10 }, }, }, }, }, }, responses: { '202': { description: 'Prijato' }, '400': { description: 'Neplatny vstup' }, }, }, }, }, }; }