Nastaveni prezije nasazeni: seed/records.json se pri prazdnem ulozisti nacte misto ukazkovych dat (zive uloziste se nikdy neprepisuje). Soubor nese soucasny stav produkce (firmy s provozovatelem, role, typ ticketu, akce, widgety, skupiny, rozlozeni). Novy GET /api/admin/export a skript npm run seed:export pro dalsi exporty, Dockerfile slozku kopiruje. Vykonnostni testy (npm run test:perf) nad 200 firmami a 10 000 tickety a zatezovy skript (npm run load) proti bezici instanci vcetne davky udalosti na webhook. Mereni odhalilo strop workeru: po obsazeni vsech mist spal sekundu, takze fronta odbavila nejvys 4 behy za sekundu. Ted ceka na prvni dokonceny beh: 500 udalosti za 1,3 s (395 behu/s). Strop posluchacu streamu zvednut na 2 000. Dialogy: prekryv modalu a menu v portalu bez backdrop-blur, tecka Zive pulzuje jen pri navazovani spojeni - rozmazani cele obrazovky pod trvalou animaci sekalo video vedle portalu. Bublina udalosti drzi 0,5 s. Dokumentace 14, 19, 20, 22, 04, 01, 03, 15 a 99 aktualizovana. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
272 lines
11 KiB
TypeScript
272 lines
11 KiB
TypeScript
/**
|
|
* Konfigurace z environment variables.
|
|
* AppFactory je predava containeru, viz AGENTS.md, sekce Variables a secrets.
|
|
* Zadna hodnota se nehardcoduje a zadny secret se neloguje.
|
|
*
|
|
* Pravidlo: chybejici konfigurace nesmi shodit start aplikace. Container,
|
|
* ktery nenastartuje, je pro AppFactory nefunkcni sluzba.
|
|
*/
|
|
|
|
import { randomBytes } from 'node:crypto';
|
|
import path from 'node:path';
|
|
|
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
|
|
function positiveNumber(value: string | undefined, fallback: number): number {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
/**
|
|
* Tajny klic pro podpis tokenu.
|
|
*
|
|
* Kdyz promenna chybi, NEPADAME a ani nepouzijeme klic zapsany v kodu.
|
|
* Aplikace musi nastartovat a odpovidat na /health, jinak ji AppFactory
|
|
* vyhodnoti jako nefunkcni. Misto toho se vygeneruje nahodny klic pro
|
|
* beh procesu a do logu jde hlasite varovani.
|
|
*
|
|
* Dusledek: po restartu containeru prestanou platit vydane tokeny
|
|
* a uzivatele se musi prihlasit znovu. Proto se JWT_SECRET ma nastavit
|
|
* jako promenna aplikace v AppFactory.
|
|
*/
|
|
function resolveJwtSecret(): string {
|
|
const value = process.env.JWT_SECRET;
|
|
if (value && value.trim().length > 0) return value;
|
|
|
|
const generated = randomBytes(32).toString('base64url');
|
|
console.warn(
|
|
'[config] JWT_SECRET neni nastavena. Pouzivam nahodny klic platny jen do restartu ' +
|
|
'containeru. Nastavte JWT_SECRET jako promennou aplikace v AppFactory.',
|
|
);
|
|
return generated;
|
|
}
|
|
|
|
/**
|
|
* Prefix verejne adresy, napr. "/apps/csbot-prototype".
|
|
* Caddy ho pred predanim do containeru odstranuje (handle_path), ale prohlizec
|
|
* ho vidi - proto se z nej sklada base pro SPA, odkazy, Swagger i webhooky.
|
|
*/
|
|
function normalizeRootPath(value: string | undefined): string {
|
|
const trimmed = (value ?? '').trim();
|
|
if (trimmed.length === 0 || trimmed === '/') return '';
|
|
const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
return withSlash.replace(/\/+$/, '');
|
|
}
|
|
|
|
/** Nenastavena promenna = vychozi cesta, prazdna = vypnuto (`''`). */
|
|
function resolveSeedFile(value: string | undefined): string {
|
|
if (value === undefined) return path.join(process.cwd(), 'seed', 'records.json');
|
|
const trimmed = value.trim();
|
|
return trimmed === '' ? '' : path.resolve(trimmed);
|
|
}
|
|
|
|
export const config = {
|
|
isProduction,
|
|
/** Port urcuje AppFactory sablona, vychozi 3000. Nemenit bez upravy metadat. */
|
|
port: Number(process.env.PORT ?? 3000),
|
|
rootPath: normalizeRootPath(process.env.ROOT_PATH),
|
|
jwtSecret: resolveJwtSecret(),
|
|
jwtExpiresIn: process.env.JWT_EXPIRES_IN ?? '8h',
|
|
/**
|
|
* Povolene originy pro CORS. V nasazeni bezi web i API na stejne domene,
|
|
* takze se CORS neuplatni. Je tu kvuli lokalnimu vyvoji s Vite dev serverem.
|
|
*/
|
|
corsOrigins: (process.env.CORS_ORIGIN ?? 'http://localhost:5173,http://localhost:4173')
|
|
.split(',')
|
|
.map((o) => o.trim())
|
|
.filter(Boolean),
|
|
/**
|
|
* Verejna adresa bez prefixu, napr. "https://services.csbot.cz".
|
|
* Sklada se z ni absolutni URL webhooku. Kdyz neni vyplnena, pouzije se
|
|
* relativni tvar - nikdy se nehardcoduje produkcni domena.
|
|
*/
|
|
publicOrigin: (process.env.PUBLIC_ORIGIN ?? '').trim().replace(/\/+$/, ''),
|
|
|
|
// ------------------------------------------------------------------ databaze
|
|
|
|
/**
|
|
* Pripojeni do Postgresu, napr. postgres://user:pass@host:5432/csbot
|
|
*
|
|
* Prazdna hodnota je platny stav: aplikace jede v pameti procesu. Container,
|
|
* ktery nenastartuje kvuli chybejici promenne, je pro AppFactory nefunkcni
|
|
* sluzba (AGENTS.md), takze se na tom nepada.
|
|
*/
|
|
databaseUrl: (process.env.DATABASE_URL ?? '').trim(),
|
|
/**
|
|
* Klic pro sifrovani pristupovych udaju konektoru.
|
|
*
|
|
* Ma to byt nahodny retezec, ne heslo. Jak ho vygenerovat je
|
|
* v documentation/14-databaze.md.
|
|
*
|
|
* Bez nej se konektory neukladaji do databaze ani kdyz je nastavena -
|
|
* plaintext v tabulce je horsi nez ztrata dat pri restartu.
|
|
*/
|
|
secretsKey: (process.env.SECRETS_KEY ?? '').trim(),
|
|
/**
|
|
* Kolik spojeni si smi vzit jedna instance.
|
|
*
|
|
* Nizke cislo je zamer: worker nesmi drzet spojeni po dobu volani ciziho API,
|
|
* takze i pri stovce soubeznych kroku staci par spojeni. Podrobnosti
|
|
* v documentation/10-runtime-a-kapacita.md.
|
|
*/
|
|
databasePoolMax: positiveNumber(process.env.DATABASE_POOL_MAX, 10),
|
|
/** Spravovane databaze vyzaduji TLS. */
|
|
databaseSsl: process.env.DATABASE_SSL === 'true',
|
|
/**
|
|
* Slozka pro data mimo databazi.
|
|
*
|
|
* Bez `DATABASE_URL` se do ni uklada JSON, ktery prezije restart procesu
|
|
* i containeru. Redeploy ho nezachova - filesystem containeru je docasny.
|
|
* Prazdna hodnota vypne i tohle a jede se v ciste pameti.
|
|
*/
|
|
dataDir: (process.env.DATA_DIR ?? path.join(process.cwd(), 'data')).trim(),
|
|
/**
|
|
* Nastaveni z repozitare pro prazdne uloziste.
|
|
*
|
|
* Nasazeni bez databaze a bez svazku prijde pri kazdem redeployi o data
|
|
* a spravce platformy zadava firmy, ucty a widgety znovu. Soubor v repu
|
|
* (vystup `npm run seed:export`) drzi konfiguraci a pri startu se pouzije
|
|
* misto vychozi sady z kodu - **jen do prazdneho uloziste**, nikdy pres
|
|
* ziva data. Chybejici soubor je bezny stav (lokalni vyvoj). Spravne reseni
|
|
* je `DATABASE_URL`, tohle je nahrada do te doby. Prazdna hodnota soubor
|
|
* vypne uplne (stejne jako u `DATA_DIR`), testy tak nezavisi na obsahu repa.
|
|
* Viz documentation/14-databaze.md.
|
|
*/
|
|
seedFile: resolveSeedFile(process.env.SEED_FILE),
|
|
|
|
// --------------------------------------------------------------- znacka
|
|
|
|
/**
|
|
* Nazev produktu. Prejmenovani = zmena teto promenne, nic jineho.
|
|
*
|
|
* Server ho pouziva v titulku Swaggeru a v OpenAPI. Klient ma svoje
|
|
* `web/src/config/brand.ts`, ktere cte `VITE_BRAND_NAME` - dosadi se pri
|
|
* buildu, protoze do prohlizece runtime promenne containeru nedosahnou.
|
|
*
|
|
* Pozor na zamenu: firma **Automia v ukazkovych datech** je zaznam
|
|
* zakaznika, ne znacka. V pozvance se ukazuje jmeno firmy, do ktere
|
|
* pozvanka zve, a to se timhle nemeni.
|
|
*/
|
|
brandName: (process.env.BRAND_NAME ?? 'WorkNuke').trim() || 'WorkNuke',
|
|
|
|
// ------------------------------------------------------- skripty konektoru
|
|
|
|
/**
|
|
* Adresar se skripty konektoru. Relativne k adresari, ze ktereho aplikace
|
|
* bezi, aby to fungovalo v containeru (`/app/connectors`) i lokalne.
|
|
*/
|
|
scriptsDir: path.resolve(process.env.SCRIPTS_DIR ?? path.join(process.cwd(), 'connectors')),
|
|
/**
|
|
* Zaklad adres napojenych sluzeb, napr. "https://services.csbot.cz/apps".
|
|
* Konkretni konektor lze presmerovat pres `<KONEKTOR>_BASE_URL`.
|
|
* Nikdy se nehardcoduje do logiky, viz AGENTS.md.
|
|
*/
|
|
servicesBaseUrl: (process.env.SERVICES_BASE_URL ?? 'https://services.csbot.cz/apps')
|
|
.trim()
|
|
.replace(/\/+$/, ''),
|
|
/**
|
|
* Odkud se zjistuje **odchozi IP adresa** tohoto containeru.
|
|
*
|
|
* Cizi sluzby maji seznamy povolenych IP a bez teto informace se neda rict,
|
|
* jestli je na nich prave nase adresa - z containeru neni videt, jak ho vidi
|
|
* protistrana. Vola se jen na vyzadani a vysledek se drzi v pameti.
|
|
*
|
|
* Prepisovatelne, aby se dalo ukazat na vlastni echo pod svou domenou misto
|
|
* na cizi sluzbu. Prazdna hodnota funkci vypne.
|
|
*/
|
|
egressIpUrl: (process.env.EGRESS_IP_URL ?? 'https://api.ipify.org?format=json').trim(),
|
|
/** Jak dlouho plati zjistena odchozi IP. Meni se nejvys pri presunu containeru. */
|
|
egressIpTtlMs: positiveNumber(process.env.EGRESS_IP_TTL_MS, 10 * 60_000),
|
|
/** Strop na jeden beh skriptu, kdyz si ho manifest neurci sam. */
|
|
scriptTimeoutMs: positiveNumber(process.env.SCRIPT_TIMEOUT_MS, 15_000),
|
|
/** Vetsi odpoved cizi sluzby se zahodi, misto aby snedla pamet procesu. */
|
|
scriptMaxResponseBytes: positiveNumber(process.env.SCRIPT_MAX_RESPONSE_BYTES, 1_000_000),
|
|
/**
|
|
* Strop na soubor odeslany z `ctx.http.postForm`.
|
|
*
|
|
* Soubor prochazi krokem stromu jako Base64, takze se cely drzi v pameti
|
|
* a zapisuje se do zaznamu behu. Nizsi cislo nez u cizich sluzeb je zamer:
|
|
* OpenAI zvladne stovky megabajtu, nas beh kroku ne.
|
|
*/
|
|
scriptMaxUploadBytes: positiveNumber(process.env.SCRIPT_MAX_UPLOAD_BYTES, 10_000_000),
|
|
/**
|
|
* Strop na text chybove odpovedi cizi sluzby.
|
|
*
|
|
* Zamerne velky. Odpoved na 401 nebo 400 obsahuje duvod a bez nej se chyba
|
|
* nedá dohledat. Chyby jsou navic vzacne, takze objem neroste jako u logu
|
|
* uspesnych kroku.
|
|
*/
|
|
errorDetailBytes: positiveNumber(process.env.SCRIPT_ERROR_DETAIL_BYTES, 8_000),
|
|
/**
|
|
* Strop na jednu strukturu (parametr typu object nebo list).
|
|
* Radek s vystupem kroku je nejrychleji rostouci tabulka v systemu, takze
|
|
* hranice patri do kontroly parametru, ne az do uklidu databaze.
|
|
*/
|
|
scriptMaxValueBytes: positiveNumber(process.env.SCRIPT_MAX_VALUE_BYTES, 256_000),
|
|
/**
|
|
* Povoli skriptum volat na localhost a do privatnich rozsahu IP.
|
|
* Jen pro lokalni vyvoj, v nasazeni musi zustat vypnute.
|
|
*/
|
|
allowPrivateTargets: process.env.ALLOW_PRIVATE_TARGETS === 'true',
|
|
/**
|
|
* Registr ARES pro zalozeni firmy podle IC nebo nazvu. Verejne API bez
|
|
* klice; promenna je tu jen pro testovaci prostredi nebo zrcadlo.
|
|
*/
|
|
aresBaseUrl: (process.env.ARES_BASE_URL ?? 'https://ares.gov.cz/ekonomicke-subjekty-v-be/rest')
|
|
.trim()
|
|
.replace(/\/+$/, ''),
|
|
/**
|
|
* Zpracovava tenhle proces frontu behu?
|
|
*
|
|
* Vychozi ano, takze jedna instance umi obojí. Az bude potreba oddelit
|
|
* vykon od API, spusti se tentyz obraz podruhe s `WORKER=1` a u API
|
|
* se nastavi `WORKER=0`.
|
|
*/
|
|
workerEnabled: process.env.WORKER !== '0',
|
|
/**
|
|
* Nasypat ukazkova data?
|
|
*
|
|
* Vychozi **ne**. Ukazkove automatizace, tickety a incidenty jsou dobre na
|
|
* predvedeni, ale na instanci, kde uz nekdo pracuje, jsou to cizi zaznamy,
|
|
* ktere se po kazdem redeployi vraceji. Konfigurace (firmy, uzivatele, role,
|
|
* resitele, typy) se nasypava vzdycky - bez ni je portal nepouzitelny.
|
|
*/
|
|
seedDemo: process.env.SEED_DEMO === '1',
|
|
/**
|
|
* Token webhooku automatizace, ktera je v seedu.
|
|
*
|
|
* Bez databaze token nikde neprezije redeploy, takze by se po kazdem
|
|
* nasazeni menila adresa a odesilatel by ji musel prepisovat. Tohle to drzi:
|
|
* token je v promenne aplikace, ne v datech ani v gitu.
|
|
*
|
|
* Kdyz neni nastaveny, vygeneruje se novy a v logu se rekne, ze se adresa
|
|
* zmenila.
|
|
*/
|
|
seedWebhookToken: (process.env.WEBHOOK_TOKEN_TEST ?? '').trim(),
|
|
/**
|
|
* Jak casto plánovač hleda, co je na case (v sekundach).
|
|
*
|
|
* Casovane spoustece nemaji sekundovou presnost a nepotrebuji ji. Kratsi
|
|
* interval znamena jen vic dotazu do fronty.
|
|
*/
|
|
schedulerIntervalSec: positiveNumber(process.env.SCHEDULER_INTERVAL_SEC, 30),
|
|
/**
|
|
* Presmerovani jedne sluzby promennou `<SLUZBA>_BASE_URL`, napr.
|
|
* `OPENAI_BASE_URL`. K cemu to je, rika `runtime/scripts/connections.ts`.
|
|
*
|
|
* Jedine misto s dynamickym nazvem promenne: sluzby pribyvaji v katalogu
|
|
* a vypisovat kazdou sem by znamenalo dve mista, ktera se rozejdou. Nazev
|
|
* sklada volajici, tady se hodnota jen cte a normalizuje - bez mezer, bez
|
|
* lomitka na konci. Prazdna nebo chybejici promenna je `null`.
|
|
*/
|
|
serviceBaseUrlOverride(variable: string): string | null {
|
|
const value = (process.env[variable] ?? '').trim().replace(/\/+$/, '');
|
|
return value === '' ? null : value;
|
|
},
|
|
};
|
|
|
|
/** Zaklad verejne adresy aplikace vcetne prefixu proxy. */
|
|
export function publicBaseUrl(): string {
|
|
return `${config.publicOrigin}${config.rootPath}`;
|
|
}
|