Nahrazeni sablony kompletnim webem a klientskym portalem

Web a portal Automia v jednom containeru. Express obsluhuje API
i zbuildovanou React aplikaci z dist/public.

Obsah:
- verejny web: homepage, sluzby, o nas, kontakt, 404
- prihlaseni pres JWT, demo ucty
- portal: prehled s grafem, tickety, incidenty, automatizace, konektory
- builder automatizaci: strom akci, vetveni podminkou
- katalog 25 konektoru v 8 kategoriich
- webhook s registrovanou adresou, token generuje server
- zivy dashboard pres SSE vcetne simulace provozu
- Swagger UI na /docs a OpenAPI na /openapi.json

Soulad s AGENTS.md:
- ROOT_PATH z prostredi, prefix proxy nikde nehardcodovan
- mount na koren i na prefix, funguje s handle_path i bez nej
- base tag a window.__BASE_PATH__ vkladane do index.html za behu
- OpenAPI servers obsahuje prefix, Try it out vola spravnou adresu
- povinne /health a /docs, port 3000, naslouchani na 0.0.0.0
- secrets jen z environment variables, nikdy v logu

Dokumentace ve slozce documentation/.
This commit is contained in:
JiriUhlir
2026-07-31 17:00:37 +02:00
parent 46f2f0b07e
commit 7b045a9f20
100 changed files with 15409 additions and 35 deletions
+600
View File
@@ -0,0 +1,600 @@
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: 'Automatizace', description: 'Sprava automatizaci a stromu akci' },
{ 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',
properties: {
id: { type: 'string', example: 'usr_1' },
email: { type: 'string', example: 'admin@automia.cz' },
name: { type: 'string', example: 'Jiri Uhlir' },
role: { type: 'string', enum: ['admin', 'client'] },
company: { type: 'string', example: 'Automia' },
},
},
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' },
},
},
Ticket: {
type: 'object',
properties: {
id: { type: 'string', example: 'TK-4821' },
subject: { type: 'string' },
requester: { type: 'string' },
status: { type: 'string', enum: ['new', 'open', 'waiting', 'resolved'] },
priority: { type: 'string', enum: ['low', 'normal', 'high', 'critical'] },
assignee: { type: 'string', nullable: true },
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
},
},
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' },
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/tickets': {
get: {
tags: ['Dashboard'],
summary: 'Seznam ticketu',
security: [{ bearerAuth: [] }],
responses: {
'200': {
description: 'Tickety',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
items: { type: 'array', items: { $ref: '#/components/schemas/Ticket' } },
},
},
},
},
},
},
},
},
'/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' } },
},
},
'/api/dashboard/connectors': {
get: {
tags: ['Automatizace'],
summary: 'Katalog konektoru',
security: [{ bearerAuth: [] }],
responses: { '200': { description: 'Konektory, kategorie a operatory podminek' } },
},
},
'/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' },
requester: { type: 'string' },
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' },
},
},
},
},
};
}