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
+80
View File
@@ -0,0 +1,80 @@
import { EventEmitter } from 'node:events';
import { randomUUID } from 'node:crypto';
/**
* Sbernice udalosti dashboardu. Vse, co zmeni data, sem posle udalost
* a pripojeni klienti ji dostanou pres SSE stream okamzite.
*
* Zamerne v pameti procesu - pri vice instancich API by tohle musel
* nahradit Redis pub/sub nebo jina sdilena fronta (viz dokumentace).
*/
export type DashboardEventType =
| 'ticket.created'
| 'ticket.updated'
| 'ticket.resolved'
| 'incident.started'
| 'incident.updated'
| 'incident.resolved'
| 'automation.created'
| 'automation.updated'
| 'automation.deleted'
| 'automation.run'
| 'webhook.received';
export interface DashboardEvent {
id: string;
type: DashboardEventType;
at: string;
/** Kratka veta pro uzivatele, zobrazuje se v notifikaci. */
message: string;
/** Doplnkova data, napr. id ticketu. */
payload?: Record<string, unknown>;
}
const CHANNEL = 'dashboard';
const MAX_RECENT = 50;
const emitter = new EventEmitter();
// Kazdy pripojeny klient je jeden listener, vychozich 10 je malo.
emitter.setMaxListeners(200);
const recent: DashboardEvent[] = [];
export function publish(
type: DashboardEventType,
message: string,
payload?: Record<string, unknown>,
): DashboardEvent {
const event: DashboardEvent = {
id: randomUUID(),
type,
at: new Date().toISOString(),
message,
payload,
};
recent.unshift(event);
if (recent.length > MAX_RECENT) recent.length = MAX_RECENT;
emitter.emit(CHANNEL, event);
console.info(`[event] ${type}: ${message} (posluchacu: ${emitter.listenerCount(CHANNEL)})`);
return event;
}
/** Vraci funkci pro odhlaseni. Volajici ji MUSI zavolat pri odpojeni. */
export function subscribe(listener: (event: DashboardEvent) => void): () => void {
emitter.on(CHANNEL, listener);
return () => {
emitter.off(CHANNEL, listener);
};
}
/** Poslednich par udalosti - klient je dostane hned po pripojeni. */
export function recentEvents(limit = 10): DashboardEvent[] {
return recent.slice(0, limit);
}
export function listenerCount(): number {
return emitter.listenerCount(CHANNEL);
}