Seed z repozitare, zatezove testy, worker bez spanku, dialogy bez rozmazani

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>
This commit is contained in:
JiriUhlir
2026-09-09 20:41:42 +02:00
co-authored by Claude Fable 5.1
parent c25e826766
commit ea9387bea9
44 changed files with 3512 additions and 28 deletions
+90
View File
@@ -0,0 +1,90 @@
/**
* Export nastaveni pres HTTP (src/routes/admin.ts, `GET /api/admin/export`):
* jen spravce platformy, vraci konfiguracni druhy vcetne hashu hesel,
* bez konektoru a provoznich dat, a zapisuje audit.
*/
import request from 'supertest';
import { beforeAll, describe, expect, test } from 'vitest';
import type { Express } from 'express';
import { createApp } from '../../src/app.js';
import { listAudit } from '../../src/data/audit.js';
import { bootstrapData } from '../../src/data/bootstrap.js';
import { SEED_KINDS } from '../../src/data/store/seedFile.js';
let app: Express;
async function login(email: string): Promise<string> {
const response = await request(app).post('/api/auth/login').send({ email, password: 'demo1234' });
expect(response.status).toBe(200);
return response.body.token as string;
}
beforeAll(async () => {
await bootstrapData({ databaseReady: false });
app = createApp();
});
describe('GET /api/admin/export', () => {
test('bez prihlaseni je 401', async () => {
expect((await request(app).get('/api/admin/export')).status).toBe(401);
});
test('spravce firmy bez platformnich prav dostane 403', async () => {
// Karel je admin u Nordisu, ale ne spravce platformy.
const token = await login('karel.vomacka@automia.cz');
const response = await request(app)
.get('/api/admin/export')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(403);
});
test('spravce platformy dostane vsechny konfiguracni druhy vcetne hashu hesel', async () => {
const token = await login('admin@automia.cz');
const response = await request(app)
.get('/api/admin/export')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(typeof response.body.exportedAt).toBe('string');
const kinds = response.body.kinds as Record<string, Array<Record<string, unknown>>>;
expect(Object.keys(kinds).sort()).toEqual([...SEED_KINDS].sort());
expect(kinds.tenant?.map((tenant) => tenant.id)).toContain('tnt_automia');
expect(kinds.role?.length).toBeGreaterThan(0);
const admin = kinds.user?.find((user) => user.email === 'admin@automia.cz');
expect(admin).toBeDefined();
expect(admin?.passwordHash).toMatch(/^\$2[aby]\$/);
});
test('konektory ani provozni data v exportu nejsou', async () => {
const token = await login('admin@automia.cz');
const response = await request(app)
.get('/api/admin/export')
.set('Authorization', `Bearer ${token}`);
const kinds = response.body.kinds as Record<string, unknown>;
for (const kind of [
'connector',
'ticket',
'incident',
'audit',
'notification',
'attachment',
'runQueue',
'invite',
]) {
expect(kinds[kind]).toBeUndefined();
}
});
test('export je v auditu jako admin.export', async () => {
const token = await login('admin@automia.cz');
await request(app).get('/api/admin/export').set('Authorization', `Bearer ${token}`);
// Audit se zapisuje bez cekani, jeden tik staci.
await new Promise((resolve) => setImmediate(resolve));
const entries = await listAudit({ tenantIds: [], includeGlobal: true, action: 'admin.export' });
expect(entries.length).toBeGreaterThan(0);
expect(entries[0]?.userEmail).toBe('admin@automia.cz');
expect(entries[0]?.detail).toMatchObject({ tenant: 3 });
});
});