Pad vykreslovani uz nesmi shodit stranku a sam zaklada incident
Ukazka tela webhooku shazovala builder pri psani cesty parametru. Chyba sama byla na jednom radku, ale to podstatne je, ze jedna vyjimka pri vykreslovani odstranila celou stranku. Uzivateli zustala bila plocha a rozdelana prace byla pryc. Aplikace na to nemela zadnou pojistku, takze totez mohlo prijit odkudkoli. Pojistka: - nova web/src/components/ErrorBoundary.tsx obaluje obsah portalu. Mistni chyba ted shodi svoji cast obrazovky, ne aplikaci: navigace, prepinac firmy i odhlaseni zustanou funkcni. Odchod jinam pojistku srovna zpatky - je to jedina trida v celem klientovi, React to jinak zachytit neumi Incident: - POST /api/dashboard/client-crash zaklada incident z padu. Bez toho je jedina stopa v konzoli prohlizece uzivatele, kam se nikdo nedostane - title a impact cte zakaznik, detail cte spravce platformy: hlaska, misto v kodu, strom komponent, adresa stranky, ucet, prohlizec a verze buildu - verze buildu je tam schvalne. U tohohle padu se ukazalo, ze bez ni se neda poznat, jestli uzivatel vidi chybu, ktera uz je opravena, nebo novou - tentyz pad na tomtez miste zalozi incident nejvys jednou za deset minut. Pad pri vykreslovani se opakuje pri kazdem prekresleni Sama ukazka: - parametr, kterym vede cesta jineho parametru, uz nedostane zastupnou hodnotu podle typu. Kdyz je jeden parametr `data` a druhy `data.result`, `data` musi byt objekt a zastupna hodnota by ho prepsala - cela ukazka je v try. Je to napoveda a nesmi shodit ani ten kus obrazovky Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
00d9d2d4b1
commit
35d1e43307
@@ -60,6 +60,12 @@ Chyba cizi sluzby neni chyba API: vraci se 200 s `ok: false` a **celym telem
|
||||
odpovedi**, protoze prave tam je napsane, co jí vadilo. "HTTP 403" samo o sobe
|
||||
nikoho nikam nedovede.
|
||||
|
||||
**Vykreslovani nesmi shodit stranku.** Jedna vyjimka pri vykreslovani odstrani
|
||||
cely strom a zustane bila plocha. Obsah portalu je proto v pojistce
|
||||
(`ErrorBoundary`) a pad z ni **zaklada incident** - bez toho je jedina stopa
|
||||
v konzoli prohlizece uzivatele. Pomocna funkce, ktera jen neco vykresluje,
|
||||
si navic ma chybu osetrit sama a degradovat, ne spadnout.
|
||||
|
||||
**Katalog je zdroj pravdy.** Co neni v `src/data/services.ts`, to nejde ulozit
|
||||
do stromu. Validace pri ukladani se pta katalogu, ne klienta.
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ React aplikaci ze slozky `dist/public`.
|
||||
| Odesilani souboru ze skriptu | hotovo | `ctx.http.postForm`, obsah jako Base64 |
|
||||
| OpenAI pod vlastnim klicem | hotovo | dotaz, soubor, prepis zvuku, seznam modelu |
|
||||
| Konektory za firmu | hotovo | pristupove udaje v konektoru, overeni napojeni |
|
||||
| Pojistka proti padu portalu | hotovo | pad shodi jen svoji cast a sam zalozi incident |
|
||||
| MCP servery firmy | hotovo | obecna sluzba a MCP EasyWebu vcetne klice zarizeni |
|
||||
| Transformace dat | hotovo | pravidla i sablona JSON, kroky si predavaji struktury |
|
||||
| Prace nad celym modelem | hotovo | ukazka tela, cesty v sablonach, smycka nad seznamem |
|
||||
|
||||
@@ -2,6 +2,53 @@
|
||||
|
||||
Nejnovejsi nahore.
|
||||
|
||||
## 2026-08-28 - pad portalu uz nesmi shodit stranku a zaklada incident
|
||||
|
||||
Ukazka tela webhooku shazovala cely builder pri psani cesty parametru. Chyba
|
||||
sama byla na jednom radku, ale to podstatne je, **ze jedna vyjimka pri
|
||||
vykreslovani odstranila celou stranku**. Uzivateli zustala bila plocha a
|
||||
rozdelana prace byla pryc. Aplikace na to nemela zadnou pojistku.
|
||||
|
||||
### Pojistka proti padu vykreslovani
|
||||
|
||||
Nova `web/src/components/ErrorBoundary.tsx` obaluje obsah portalu. Mistni chyba
|
||||
ted shodi svoji cast obrazovky, ne aplikaci: navigace, prepinac firmy
|
||||
i odhlaseni zustanou funkcni a uzivatel ma kam odejit. Odchod na jinou stranku
|
||||
pojistku srovna zpatky.
|
||||
|
||||
Je to jedina trida v celem klientovi. Hook na tohle neni a nebude, React to
|
||||
umi zachytit jen takhle.
|
||||
|
||||
### Pad zaklada incident
|
||||
|
||||
`POST /api/dashboard/client-crash`. Bez toho je jedina stopa v konzoli
|
||||
prohlizece uzivatele, kam se nikdo nedostane - takze bychom o padu vedeli jen
|
||||
tehdy, kdyby ho nekdo nahlasil. To znamena o vetsine padu nevedet.
|
||||
|
||||
Incident nese dve casti, stejne jako ostatni incidenty:
|
||||
|
||||
- `title` a `impact` cte zakaznik, tedy zadne stack trace,
|
||||
- `detail` cte spravce platformy: hlaska, misto v kodu, strom komponent, adresa
|
||||
stranky, ucet, prohlizec a **verze buildu**.
|
||||
|
||||
Verze buildu je tam schvalne. U tohohle padu se ukazalo, ze bez ni se neda
|
||||
poznat, jestli uzivatel vidi chybu, ktera uz je opravena, nebo novou.
|
||||
|
||||
Tentyz pad na tomtez miste zalozi incident nejvys jednou za deset minut. Pad
|
||||
pri vykreslovani se opakuje pri kazdem prekresleni a jinak by z jedne chyby
|
||||
vzniklo padesat incidentu a ten pravy by v nich zapadl.
|
||||
|
||||
### Sama ukazka
|
||||
|
||||
- Do cesty parametru se da zanorit vzdy. Kdyz na miste je skalar, nahradi se
|
||||
schrankou - driv se zapisovalo do retezce a to shodilo stranku.
|
||||
- Parametr, kterym vede cesta jineho parametru, uz nedostane zastupnou hodnotu
|
||||
podle typu. Plati vnitrni struktura: kdyz je jeden parametr `data` a druhy
|
||||
`data.result`, **`data` musi byt objekt**. Zastupna hodnota by ho prepsala.
|
||||
- Cela ukazka je navic v `try`. Je to napoveda a nesmi shodit ani ten kus
|
||||
obrazovky, at uz do ni prijde cokoliv - parametry se pisou znak po znaku
|
||||
a rozdelany stav je normalni.
|
||||
|
||||
## 2026-08-28 - MCP EasyWeb podle skutecne specifikace (auth v2)
|
||||
|
||||
Predchozi verze posilala na `/login` jen jmeno, heslo a nazev zarizeni. Server
|
||||
|
||||
@@ -1785,6 +1785,57 @@ export function buildOpenApiDocument() {
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/client-crash': {
|
||||
post: {
|
||||
tags: ['Portal'],
|
||||
summary: 'Nahlasit pad vykreslovani',
|
||||
description:
|
||||
'Zaklada incident z padu portalu v prohlizeci. Bez toho je jedina stopa v konzoli ' +
|
||||
'uzivatele, kam se nikdo nedostane, takze bychom o padu vedeli jen tehdy, kdyby ho ' +
|
||||
'nekdo nahlasil. Incident nese title a impact pro zakaznika a detail pro spravce ' +
|
||||
'platformy: hlaska, misto v kodu, strom komponent, adresa stranky a verze buildu. ' +
|
||||
'Tentyz pad na tomtez miste zalozi incident nejvys jednou za deset minut - pad pri ' +
|
||||
'vykreslovani se opakuje pri kazdem prekresleni a jinak by z jedne chyby vzniklo ' +
|
||||
'padesat incidentu. Volá to pojistka v klientovi, ne clovek.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['message'],
|
||||
properties: {
|
||||
message: { type: 'string' },
|
||||
stack: { type: 'string' },
|
||||
componentStack: { type: 'string' },
|
||||
path: { type: 'string', description: 'Kde v portalu se to stalo.' },
|
||||
build: { type: 'string', description: 'Verze nasazeneho klienta.' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'201': {
|
||||
description: 'Incident zalozen',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
created: { type: 'boolean' },
|
||||
incidentId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'202': { description: 'Stejny pad uz je hlaseny, incident se nezaklada' },
|
||||
'400': { description: 'Neplatny vstup' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/connectors/{id}/mcp/tools': {
|
||||
post: {
|
||||
tags: ['Konektory'],
|
||||
|
||||
+102
-1
@@ -41,7 +41,7 @@ import {
|
||||
} from '../data/dashboardLayouts.js';
|
||||
import { collectScopes } from '../data/flowScope.js';
|
||||
import { widgetCatalog } from '../data/widgets.js';
|
||||
import { listIncidents } from '../data/incidentStore.js';
|
||||
import { createIncident, listIncidents } from '../data/incidentStore.js';
|
||||
import { getSummary } from '../data/mock.js';
|
||||
import { findPersonByEmail, groupsOfPerson, listGroups, listPeople } from '../data/people.js';
|
||||
import { hasPermission } from '../data/permissions.js';
|
||||
@@ -139,6 +139,107 @@ dashboardRouter.get('/incidents', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Hlaseni padu portalu.
|
||||
*
|
||||
* Kdyz v prohlizeci spadne vykreslovani, uzivatel to vidi, ale my ne. Bez
|
||||
* tohohle endpointu je jedina stopa v jeho konzoli, kam se nikdo nedostane,
|
||||
* takze o padu vime jen tehdy, kdyz ho nekdo nahlasi. To znamena, ze o vetsine
|
||||
* padu nevime vubec.
|
||||
*
|
||||
* Incident nese dve casti a je to zamerne:
|
||||
* - `title` a `impact` cte zakaznik, takze zadne stack trace,
|
||||
* - `detail` cte spravce platformy a je v nem vsechno: hlaska, misto v kodu,
|
||||
* strom komponent, adresa stranky a verze buildu.
|
||||
*
|
||||
* Cely `detail` je to, **jak se to stalo**. Bez adresy a stromu komponent je
|
||||
* hlaska "Cannot create property" k nepouziti.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Kolik nejvys znaku se z jednoho pole prevezme.
|
||||
*
|
||||
* Strom komponent umi byt velmi dlouhy a do incidentu patri jeho zacatek,
|
||||
* protoze prave nahore je komponenta, ktera spadla.
|
||||
*/
|
||||
const FIELD_LIMIT = 4_000;
|
||||
|
||||
/**
|
||||
* Jak dlouho se tentyz pad povazuje za jeden.
|
||||
*
|
||||
* Pad pri vykreslovani se opakuje pri kazdem stisku klavesy. Bez tohohle by
|
||||
* z jedne chyby vzniklo padesat incidentu a ten pravy by v nich zapadl.
|
||||
*/
|
||||
const DEDUPE_MS = 600_000;
|
||||
|
||||
/** Otisk padu a kdy naposled zalozil incident. */
|
||||
const recent = new Map<string, number>();
|
||||
|
||||
const crashSchema = z.object({
|
||||
message: z.string().trim().min(1).max(FIELD_LIMIT),
|
||||
stack: z.string().max(FIELD_LIMIT).optional(),
|
||||
componentStack: z.string().max(FIELD_LIMIT).optional(),
|
||||
/** Kde v portalu se to stalo. Bez toho se to nema kde hledat. */
|
||||
path: z.string().max(500).optional(),
|
||||
/** Verze nasazeneho klienta. Rika, jestli uz je v tom oprava. */
|
||||
build: z.string().max(200).optional(),
|
||||
});
|
||||
|
||||
dashboardRouter.post('/client-crash', (req, res) => {
|
||||
const access = accessFor(req.user!);
|
||||
const requested = typeof req.query.tenantId === 'string' ? req.query.tenantId : undefined;
|
||||
const tenantId = requested ?? access.defaultTenantId;
|
||||
|
||||
const parsed = crashSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: parsed.error.issues[0]?.message ?? 'Neplatný vstup.',
|
||||
});
|
||||
}
|
||||
|
||||
const crash = parsed.data;
|
||||
const where = crash.path ?? 'neznámá stránka';
|
||||
|
||||
/*
|
||||
* Otisk je hlaska a misto, ne cas. Tatáz chyba na tomtez miste je porad
|
||||
* jeden problem, i kdyz na ni clovek narazi desetkrat za sebou.
|
||||
*/
|
||||
const fingerprint = `${crash.message}|${where}`;
|
||||
const last = recent.get(fingerprint) ?? 0;
|
||||
const now = Date.now();
|
||||
if (now - last < DEDUPE_MS) {
|
||||
return res.status(202).json({ created: false, reason: 'stejný pád už je hlášený' });
|
||||
}
|
||||
recent.set(fingerprint, now);
|
||||
|
||||
const detail = [
|
||||
`Stránka: ${where}`,
|
||||
`Uživatel: ${req.user!.email}`,
|
||||
`Verze klienta: ${crash.build ?? 'neznámá'}`,
|
||||
`Prohlížeč: ${String(req.headers['user-agent'] ?? 'neznámý')}`,
|
||||
'',
|
||||
`Hláška: ${crash.message}`,
|
||||
...(crash.componentStack ? ['', 'Strom komponent:', crash.componentStack] : []),
|
||||
...(crash.stack ? ['', 'Zásobník volání:', crash.stack] : []),
|
||||
].join('\n');
|
||||
|
||||
const incident = createIncident({
|
||||
tenantId,
|
||||
title: 'Část portálu se nepodařilo vykreslit',
|
||||
service: 'Portál',
|
||||
// Pad vykreslovani neni vypadek sluzby, ale uzivatel u toho nemuze
|
||||
// pokracovat v praci. Prostredni zavaznost, ne nejvyssi.
|
||||
severity: 'sev2',
|
||||
impact: `Stránka ${where} se části uživatelů nezobrazila správně. Ostatní části portálu fungují.`,
|
||||
detail,
|
||||
source: 'portál',
|
||||
});
|
||||
|
||||
console.error(`[ui] pad portalu na ${where}: ${crash.message} (incident ${incident.id})`);
|
||||
return res.status(201).json({ created: true, incidentId: incident.id });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------- rozlozeni dashboardu
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
||||
import { AlertTriangle, RotateCcw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { buildInfo } from '@/config/version';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
|
||||
/**
|
||||
* Pojistka proti pádu vykreslovani.
|
||||
*
|
||||
* React nema mezistav: **jedna vyjimka pri vykreslovani odstrani cely strom**
|
||||
* a uzivateli zustane bila stranka bez jakekoliv hlasky. Nezachrani ho ani
|
||||
* obnoveni, protoze se vykresli totez. U rozdelane prace v builderu to znamena,
|
||||
* ze o ni prisel.
|
||||
*
|
||||
* Zachytit se to da jedine takhle, tridni komponentou - hook na to neni
|
||||
* a nebude. Proto je tenhle soubor jediny, kde je v celem klientovi trida.
|
||||
*
|
||||
* Neni to nahrada za osetrene chyby. Je to posledni zabrana pro to, na co se
|
||||
* neprislo: mistni chyba ma shodit svoji cast obrazovky, ne aplikaci.
|
||||
*/
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
/** Co se ukaze misto spadle casti. Chybi = vychozi hlaska nize. */
|
||||
fallback?: ReactNode;
|
||||
/**
|
||||
* Zmena teto hodnoty pojistku shodi zpatky do poradku.
|
||||
*
|
||||
* Pouziva se adresa stranky: kdyz uzivatel odejde jinam, nema tam na nej
|
||||
* cekat chyba z mista, kde uz neni.
|
||||
*/
|
||||
resetKey?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Co uz bylo nahlaseno.
|
||||
*
|
||||
* Pad pri vykreslovani se opakuje pri kazdem prekresleni, takze bez tohohle by
|
||||
* z jedne chyby bylo padesat volani na server. Server si stejnou chybu
|
||||
* odfiltruje taky, tohle je jen proto, aby se zbytecne neposilala.
|
||||
*/
|
||||
const reported = new Set<string>();
|
||||
|
||||
/**
|
||||
* Zalozi incident z padu vykreslovani.
|
||||
*
|
||||
* Bez toho je jedina stopa v konzoli prohlizece uzivatele, kam se nikdo
|
||||
* nedostane - takze bychom o padu vedeli jen tehdy, kdyby ho nekdo nahlasil.
|
||||
* To znamena o vetsine padu nevedet.
|
||||
*
|
||||
* **Nesmi vyhodit.** Chyba pri hlaseni chyby je to posledni, co ma uzivatel
|
||||
* videt, takze se jen zapise do konzole.
|
||||
*/
|
||||
function report(error: Error, info: ErrorInfo): void {
|
||||
const path = `${window.location.pathname}${window.location.search}`;
|
||||
const key = `${error.message}|${path}`;
|
||||
if (reported.has(key)) return;
|
||||
reported.add(key);
|
||||
|
||||
void apiFetch('/api/dashboard/client-crash', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
message: error.message,
|
||||
stack: error.stack?.slice(0, 4_000),
|
||||
componentStack: info.componentStack?.slice(0, 4_000) ?? undefined,
|
||||
path,
|
||||
build: `${buildInfo.version} ${buildInfo.builtAt}${buildInfo.commit ? ` ${buildInfo.commit}` : ''}`,
|
||||
},
|
||||
}).catch((err: unknown) => {
|
||||
console.error('[ui] pad se nepodarilo nahlasit:', err);
|
||||
});
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
override state: State = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { error };
|
||||
}
|
||||
|
||||
override componentDidUpdate(previous: Props): void {
|
||||
// Jina stranka, cista tabule. Bez toho by chyba drzela i po odchodu jinam.
|
||||
if (previous.resetKey !== this.props.resetKey && this.state.error) {
|
||||
this.setState({ error: null });
|
||||
}
|
||||
}
|
||||
|
||||
override componentDidCatch(error: Error, info: ErrorInfo): void {
|
||||
/*
|
||||
* Do konzole cele, vcetne toho, ktera komponenta to byla. Bez toho zbyde
|
||||
* jen zmensene jmeno funkce a hledani zacina od nuly.
|
||||
*/
|
||||
console.error('[ui] vykreslovani spadlo:', error, info.componentStack);
|
||||
report(error, info);
|
||||
}
|
||||
|
||||
override render(): ReactNode {
|
||||
const { error } = this.state;
|
||||
if (!error) return this.props.children;
|
||||
if (this.props.fallback !== undefined) return this.props.fallback;
|
||||
|
||||
return (
|
||||
<div className="glass rounded-card border border-danger-500/40 p-5">
|
||||
<p className="flex items-center gap-2 font-semibold text-danger-400">
|
||||
<AlertTriangle className="size-4 shrink-0" />
|
||||
Tuhle část se nepodařilo vykreslit
|
||||
</p>
|
||||
<p className="mt-1.5 text-sm text-white/60">
|
||||
Zbytek portálu funguje dál. Zkuste to znovu, a když to bude přetrvávat,
|
||||
pošlete nám prosím tuhle hlášku.
|
||||
</p>
|
||||
<p className="mt-2 font-mono text-xs break-all text-white/35">{error.message}</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => this.setState({ error: null })}
|
||||
>
|
||||
<RotateCcw className="size-3.5" />
|
||||
Zkusit znovu
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { ErrorBoundary } from '@/components/ErrorBoundary';
|
||||
import { useAuth, type AuthUser } from '@/auth/AuthContext';
|
||||
import { EventStreamProvider } from '@/components/dashboard/EventStreamProvider';
|
||||
import { EventToasts } from '@/components/dashboard/EventToasts';
|
||||
@@ -323,8 +324,20 @@ function DashboardShell() {
|
||||
Ztrata rozepsaneho stavu je pritom spravne. Filtr na cloveka z firmy A
|
||||
nema ve firme B co znamenat.
|
||||
*/}
|
||||
{/*
|
||||
Pojistka kolem obsahu stranky.
|
||||
|
||||
Vyjimka pri vykreslovani jinak odstrani cely strom a zustane bila
|
||||
stranka. Takhle spadne jen obsah, kdezto navigace, prepinac firmy
|
||||
i odhlaseni zustanou funkcni - uzivatel ma kam odejit.
|
||||
|
||||
`resetKey` je adresa: po odchodu jinam uz na nej nema cekat chyba
|
||||
z mista, kde neni.
|
||||
*/}
|
||||
<main key={activeTenant ?? 'bez-firmy'} className="px-5 py-7 sm:px-7">
|
||||
<Outlet />
|
||||
<ErrorBoundary resetKey={location.pathname}>
|
||||
<Outlet />
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -283,6 +283,21 @@ function ProvidedFields({
|
||||
* ukaze zanoreny presne tam. Jinak by odesilatel poslal plocho a nic by nesedelo.
|
||||
*/
|
||||
function exampleBody(fields: TriggerField[]): string {
|
||||
try {
|
||||
return buildExample(fields);
|
||||
} catch (err) {
|
||||
/*
|
||||
* Ukazka je napoveda. **Nesmi shodit stranku**, at uz do ni prijde
|
||||
* cokoliv - parametry se pisou znak po znaku a rozdelany stav je normalni.
|
||||
* Pojistka kolem obsahu portalu to zachyti taky, ale tam uz zmizi cely
|
||||
* builder i s rozdelanou praci.
|
||||
*/
|
||||
console.error('[builder] ukazku tela se nepodarilo sestavit:', err);
|
||||
return '{\n "cokoliv": "ukázku nejde sestavit, zkontrolujte cesty parametrů"\n}';
|
||||
}
|
||||
}
|
||||
|
||||
function buildExample(fields: TriggerField[]): string {
|
||||
if (fields.length === 0) return '{\n "cokoliv": "co pošle odesílatel"\n}';
|
||||
|
||||
const body: Record<string, unknown> = {};
|
||||
|
||||
Reference in New Issue
Block a user