U posledniho volani je videt, co presne prislo
Prvni verze seznamu poslednich volani ukazovala cas a duvod odmitnuti, ale ne data. To je k nicemu: "parametr data ma mit typ text" nerekne, co tedy prislo, a bez toho zbyva hadat. Argument, ze se telo nema drzet kvuli zakaznickym datum, navic neobstal. Tickety si cela prijata tela u udalosti drzi uz davno, takze si to jedno misto zakazovalo neco, co aplikace jinde bezne dela - a zaplatilo za to celym uzitkem te funkce. Radek volani se ted rozklikne na dve veci: - rozpad po parametrech: jmeno, cesta, co se cekalo a co na te ceste opravdu bylo, vcetne nahledu hodnoty. Sklada ho readPayload, protoze jen tam je videt kontrakt i telo naraz - pozdeji uz se kontrakt muze zmenit - cele prijate telo, vcetne klicu, ktere odesilatel posila navic a kontrakt o nich nevi. Delsi nez 8 kB se usekne Telo se do zaznamu zmrazi na retezec, aby se pozdeji nezmenilo pod rukama, a serializace je v try - cyklicka struktura nesmi shodit prijem webhooku. Overeno na bezici instanci s vlastnim DATA_DIR, telem s data jako textem misto objektu a bez povinneho status: ok callSid | cekame string, povinny | prislo text = CA-B CHYBA status | cekame string, povinny | prislo neprislo CHYBA data | cekame object | prislo text = tohle je text misto objektu Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e8bb4d6e54
commit
40875439e4
@@ -523,13 +523,26 @@ vsechno v poradku.
|
||||
nema nikdo spadnout omylem. Zaskrtnout to jde vzdycky.
|
||||
- **Objekt a seznam jdou vybrat.** `declarableFieldTypes` je ma a klient si uz
|
||||
nedrzi vlastni kopii, bere sdileny seznam.
|
||||
- **Poslednich deset volani je videt na detailu automatizace.** Cas, jestli
|
||||
proslo nebo ne, a u odmitnutych duvod.
|
||||
- **Poslednich deset volani je videt na detailu automatizace.** Radek se
|
||||
rozklikne na rozpad po parametrech a cele prijate telo.
|
||||
|
||||
Telo se **schvalne neuklada**. Duvod odmitnuti uz rika, co je spatne ("Parametr
|
||||
data ma mit typ string"), a drzet payloady by znamenalo mit v pameti kopie
|
||||
zakaznickych dat, aniz by o tom kdokoliv vedel. Seznam volani se drzi v pameti,
|
||||
restart ho zahodi - je to diagnostika posledni hodiny, ne historie.
|
||||
Samotny duvod odmitnuti nestaci a prvni verze na tom ztroskotala: "parametr data
|
||||
ma mit typ text" nerekne, co tedy prislo, a bez toho zbyva hadat. Rozpad ukaze
|
||||
u kazdeho parametru jeho cestu, co se cekalo a **co na te ceste opravdu bylo**:
|
||||
|
||||
```text
|
||||
ok callSid | cekame string, povinny | prislo text = CA-B
|
||||
CHYBA status | cekame string, povinny | prislo nepřišlo
|
||||
CHYBA data | cekame object | prislo text = tohle je text misto objektu
|
||||
```
|
||||
|
||||
K tomu cele telo, vcetne klicu, ktere odesilatel posila navic a kontrakt o nich
|
||||
nevi. Delsi nez 8 kB se usekne.
|
||||
|
||||
Neni to novy druh dat: tickety si cela prijata tela u udalosti drzi uz davno.
|
||||
Jen se konecne daji videt i u volani, ktera neprosla, tedy prave tam, kde je to
|
||||
potreba. Seznam se drzi v pameti, restart ho zahodi - je to diagnostika posledni
|
||||
hodiny, ne historie.
|
||||
|
||||
### Co se tim nedela
|
||||
|
||||
|
||||
@@ -2,6 +2,31 @@
|
||||
|
||||
Nejnovejsi nahore.
|
||||
|
||||
## 2026-09-02 - U posledniho volani je videt, co presne prislo
|
||||
|
||||
Prvni verze seznamu poslednich volani ukazovala cas a duvod odmitnuti, ale ne
|
||||
data. To je k nicemu: "parametr data ma mit typ text" nerekne, co tedy prislo,
|
||||
a bez toho zbyva hadat. Argument, ze se telo nema drzet kvuli zakaznickym datum,
|
||||
navic neobstal - tickety si cela prijata tela u udalosti drzi uz davno, takze si
|
||||
to jedno misto zakazovalo neco, co aplikace jinde bezne dela.
|
||||
|
||||
Radek volani se ted rozklikne na dve veci:
|
||||
|
||||
- **rozpad po parametrech**: jmeno, cesta, co se cekalo a co na te ceste opravdu
|
||||
bylo, vcetne nahledu hodnoty. Sklada ho `readPayload`, protoze jen tam je
|
||||
videt kontrakt i telo naraz - pozdeji uz se kontrakt muze zmenit
|
||||
- **cele prijate telo**, vcetne klicu, ktere odesilatel posila navic a kontrakt
|
||||
o nich nevi. Delsi nez 8 kB se usekne
|
||||
|
||||
```text
|
||||
ok callSid | cekame string, povinny | prislo text = CA-B
|
||||
CHYBA status | cekame string, povinny | prislo nepřišlo
|
||||
CHYBA data | cekame object | prislo text = tohle je text misto objektu
|
||||
```
|
||||
|
||||
Telo se do zaznamu zmrazi na retezec, aby se pozdeji nezmenilo pod rukama, a
|
||||
serializace je v `try` - cyklicka struktura nesmi shodit prijem webhooku.
|
||||
|
||||
## 2026-09-02 - Kontrakt webhooku: objekt jde vybrat a odmitnuti je videt
|
||||
|
||||
Parametr spoustece `data` byl deklarovany jako `type: string, required: true`,
|
||||
|
||||
@@ -170,6 +170,22 @@ export interface Automation {
|
||||
issues: string[];
|
||||
}
|
||||
|
||||
/** Jak dopadl jeden deklarovany parametr v jednom volani. */
|
||||
export interface WebhookCallField {
|
||||
name: string;
|
||||
/** Cesta v tele. Shodna se jmenem, kdyz se nezadala vlastni. */
|
||||
path: string;
|
||||
required: boolean;
|
||||
/** Deklarovany typ z kontraktu. */
|
||||
type: string;
|
||||
/** Co na te ceste opravdu bylo: text, cislo, objekt, seznam, nepreslo. */
|
||||
got: string;
|
||||
/** Kratky nahled prijate hodnoty. */
|
||||
preview: string;
|
||||
/** false = tenhle parametr je duvod, proc se volani odmitlo. */
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jedno volani webhooku, jak dopadlo.
|
||||
*
|
||||
@@ -177,9 +193,10 @@ export interface Automation {
|
||||
* nedostane. Odesilatel dostal 400 a vedel o tom, ale ten, kdo kontrakt napsal,
|
||||
* se nedozvedel nic - automatizace svitila zelene a jen do ni nic nechodilo.
|
||||
*
|
||||
* Telo se **schvalne neuklada**. Duvod odmitnuti uz rika, co je spatne
|
||||
* ("Parametr data ma mit typ string"), a drzet payloady by znamenalo mit
|
||||
* v pameti kopie zakaznickych dat bez toho, aby o tom kdokoliv vedel.
|
||||
* Drzi se **cele prijate telo i rozpad po parametrech**. Duvod odmitnuti sam
|
||||
* nestaci: "parametr data ma mit typ text" nerekne, co tedy prislo, a bez toho
|
||||
* zbyva hadat. Tickety si cela prijata tela u udalosti drzi uz davno, takze to
|
||||
* neni novy druh dat, jen se konecne daji videt i u volani, ktera neprosla.
|
||||
*/
|
||||
export interface WebhookCall {
|
||||
at: string;
|
||||
@@ -189,6 +206,28 @@ export interface WebhookCall {
|
||||
problems: string[];
|
||||
/** Beh, ktery z volani vznikl. null u odmitnutych a u duplicit. */
|
||||
runId: string | null;
|
||||
/** Kazdy deklarovany parametr a jak dopadl. */
|
||||
fields: WebhookCallField[];
|
||||
/** Cele prijate telo jako JSON. Delsi nez `MAX_BODY` se usekne. */
|
||||
body: string;
|
||||
/** true = telo bylo delsi a je useknute. */
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
/** Kolik znaku tela se u volani drzi. Vic uz je v pameti zbytecne. */
|
||||
const MAX_BODY = 8_000;
|
||||
|
||||
/** Telo do zaznamu: zmrazene na retezec, aby se pozdeji nezmenilo pod rukama. */
|
||||
export function bodyForCall(body: unknown): { body: string; truncated: boolean } {
|
||||
let text: string;
|
||||
try {
|
||||
text = JSON.stringify(body, null, 2) ?? '';
|
||||
} catch {
|
||||
// Cyklicka struktura nebo neco, co JSON neumi. Radeji nic nez pad.
|
||||
text = '';
|
||||
}
|
||||
if (text.length <= MAX_BODY) return { body: text, truncated: false };
|
||||
return { body: text.slice(0, MAX_BODY), truncated: true };
|
||||
}
|
||||
|
||||
/** Kolik poslednich volani se u automatizace drzi. */
|
||||
|
||||
+48
-4
@@ -1,9 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
bodyForCall,
|
||||
findByWebhookToken,
|
||||
recordWebhookCall,
|
||||
type TriggerField,
|
||||
type WebhookCallField,
|
||||
} from '../data/automationStore.js';
|
||||
import type { FieldType } from '../data/conditions.js';
|
||||
import { findByIntakeToken } from '../data/tenants.js';
|
||||
@@ -191,7 +193,8 @@ webhookRouter.post('/:token', (req, res) => {
|
||||
}
|
||||
|
||||
const body = (req.body ?? {}) as Record<string, unknown>;
|
||||
const { values, problems } = readPayload(automation.flow.trigger.fields, body);
|
||||
const { values, problems, report } = readPayload(automation.flow.trigger.fields, body);
|
||||
const received = bodyForCall(body);
|
||||
|
||||
if (problems.length > 0) {
|
||||
console.warn(`[webhook] ${automation.id}: neplatna data - ${problems.join(' ')}`);
|
||||
@@ -206,6 +209,8 @@ webhookRouter.post('/:token', (req, res) => {
|
||||
ok: false,
|
||||
problems,
|
||||
runId: null,
|
||||
fields: report,
|
||||
...received,
|
||||
});
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
@@ -247,6 +252,8 @@ webhookRouter.post('/:token', (req, res) => {
|
||||
ok: true,
|
||||
problems: [],
|
||||
runId: item?.id ?? null,
|
||||
fields: report,
|
||||
...received,
|
||||
});
|
||||
// 202: prevzato, zpracuje se. Ne 200, ktera by rikala "hotovo".
|
||||
return res.status(202).json({
|
||||
@@ -308,14 +315,33 @@ webhookRouter.get('/:token', (req, res) => {
|
||||
function readPayload(
|
||||
fields: TriggerField[],
|
||||
body: Record<string, unknown>,
|
||||
): { values: Record<string, unknown>; problems: string[] } {
|
||||
): { values: Record<string, unknown>; problems: string[]; report: WebhookCallField[] } {
|
||||
const values: Record<string, unknown> = {};
|
||||
const problems: string[] = [];
|
||||
/*
|
||||
* Zprava o kazdem parametru, at uz dopadl jakkoliv. Duvod odmitnuti sam
|
||||
* nestaci: "parametr data ma mit typ text" nerekne, co tedy prislo, a bez
|
||||
* toho se hada. Sklada se tady, protoze jen tady je videt kontrakt i telo
|
||||
* naraz - pozdeji uz se kontrakt muze zmenit.
|
||||
*/
|
||||
const report: WebhookCallField[] = [];
|
||||
|
||||
for (const field of fields) {
|
||||
const value = getPath(body, field.path ?? field.name);
|
||||
const path = field.path ?? field.name;
|
||||
const value = getPath(body, path);
|
||||
const entry: WebhookCallField = {
|
||||
name: field.name,
|
||||
path,
|
||||
required: field.required,
|
||||
type: field.type,
|
||||
got: describeIncoming(value),
|
||||
preview: previewValue(value),
|
||||
ok: true,
|
||||
};
|
||||
report.push(entry);
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
entry.ok = !field.required;
|
||||
if (field.required) {
|
||||
const where = field.path && field.path !== field.name ? ` (cesta ${field.path})` : '';
|
||||
problems.push(`Chybí povinný parametr "${field.name}"${where}.`);
|
||||
@@ -324,6 +350,7 @@ function readPayload(
|
||||
}
|
||||
|
||||
if (!matchesType(value, field.type)) {
|
||||
entry.ok = false;
|
||||
problems.push(`Parametr "${field.name}" má mít typ ${field.type}.`);
|
||||
continue;
|
||||
}
|
||||
@@ -331,7 +358,24 @@ function readPayload(
|
||||
values[field.name] = value;
|
||||
}
|
||||
|
||||
return { values, problems };
|
||||
return { values, problems, report };
|
||||
}
|
||||
|
||||
/** Co na te ceste opravdu bylo. Bez toho se z hlasky o typu neda nic poznat. */
|
||||
function describeIncoming(value: unknown): string {
|
||||
if (value === undefined) return 'nepřišlo';
|
||||
if (value === null) return 'null';
|
||||
if (Array.isArray(value)) return 'seznam';
|
||||
if (typeof value === 'object') return 'objekt';
|
||||
return typeof value === 'string' ? 'text' : typeof value === 'number' ? 'číslo' : 'ano/ne';
|
||||
}
|
||||
|
||||
/** Kratky nahled hodnoty do tabulky. Cele telo je u volani zvlast. */
|
||||
function previewValue(value: unknown): string {
|
||||
if (value === undefined) return '';
|
||||
const text = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
if (typeof text !== 'string') return '';
|
||||
return text.length > 80 ? `${text.slice(0, 80)}...` : text;
|
||||
}
|
||||
|
||||
/** Ukazkove telo podle kontraktu, at je videt, jak to ma vypadat. */
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { AlertCircle, ArrowLeft, Check, Info, Pause, Play, Save, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowLeft,
|
||||
Check,
|
||||
ChevronRight,
|
||||
Info,
|
||||
Pause,
|
||||
Play,
|
||||
Save,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||
import { DataState } from '@/components/dashboard/DataState';
|
||||
@@ -7,6 +17,7 @@ import { StepPicker } from '@/components/dashboard/flow/StepPicker';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
import { cn } from '@/lib/cn';
|
||||
import {
|
||||
countSteps,
|
||||
createActionStep,
|
||||
@@ -477,12 +488,15 @@ export default function AutomationDetail() {
|
||||
*
|
||||
* Odmitnute volani do ted skoncilo jako radek v logu kontejneru. Odesilatel
|
||||
* dostal 400 a vedel o tom, ale ten, kdo kontrakt psal, se nedozvedel nic -
|
||||
* automatizace svitila zelene a jen do ni prestalo chodit. Tohle je to nejmensi,
|
||||
* co staci: kdyz se implementator ozve, ze mu neco nesedi, je tady videt co.
|
||||
* automatizace svitila zelene a jen do ni prestalo chodit.
|
||||
*
|
||||
* Telo se neuklada, jen duvod odmitnuti. Ten uz rika, co je spatne.
|
||||
* Radek se rozklikne na **rozpad po parametrech a cele prijate telo**. Samotny
|
||||
* duvod odmitnuti nestaci: "parametr data ma mit typ text" nerekne, co tedy
|
||||
* prislo, a bez toho zbyva hadat.
|
||||
*/
|
||||
function WebhookCalls({ calls }: { calls: WebhookCall[] }) {
|
||||
const [openAt, setOpenAt] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div className="glass rounded-card p-5">
|
||||
<h2 className="font-semibold text-white">Poslední volání</h2>
|
||||
@@ -493,24 +507,88 @@ function WebhookCalls({ calls }: { calls: WebhookCall[] }) {
|
||||
která jsme odmítli.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="mt-3 space-y-2 text-sm">
|
||||
{calls.map((call) => (
|
||||
<li key={`${call.at}-${call.runId ?? 'x'}`} className="flex gap-2">
|
||||
{call.ok ? (
|
||||
<Check className="mt-0.5 size-3.5 shrink-0 text-emerald-400" />
|
||||
) : (
|
||||
<AlertCircle className="mt-0.5 size-3.5 shrink-0 text-rose-400" />
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<span className="text-white/70">{formatDateTime(call.at)}</span>
|
||||
{call.problems.length > 0 && (
|
||||
<p className="text-xs leading-relaxed text-rose-300/80">
|
||||
{call.problems.join(' ')}
|
||||
</p>
|
||||
<ul className="mt-3 space-y-1.5 text-sm">
|
||||
{calls.map((call) => {
|
||||
const open = openAt === call.at;
|
||||
return (
|
||||
<li key={call.at} className="rounded-lg border border-ink-600/50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenAt(open ? null : call.at)}
|
||||
className="flex w-full items-start gap-2 px-3 py-2 text-left hover:bg-ink-800/40"
|
||||
>
|
||||
{call.ok ? (
|
||||
<Check className="mt-0.5 size-3.5 shrink-0 text-emerald-400" />
|
||||
) : (
|
||||
<AlertCircle className="mt-0.5 size-3.5 shrink-0 text-rose-400" />
|
||||
)}
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="text-white/70">{formatDateTime(call.at)}</span>
|
||||
{call.problems.length > 0 && (
|
||||
<span className="block text-xs leading-relaxed text-rose-300/80">
|
||||
{call.problems.join(' ')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<ChevronRight
|
||||
className={cn('mt-0.5 size-3.5 shrink-0 text-white/30', open && 'rotate-90')}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="space-y-3 border-t border-ink-600/50 px-3 py-3">
|
||||
{call.fields.length > 0 && (
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-left text-white/35">
|
||||
<th className="pb-1 font-normal">Parametr</th>
|
||||
<th className="pb-1 font-normal">Čekáme</th>
|
||||
<th className="pb-1 font-normal">Přišlo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{call.fields.map((field) => (
|
||||
<tr
|
||||
key={field.name}
|
||||
className={field.ok ? 'text-white/60' : 'text-rose-300'}
|
||||
>
|
||||
<td className="py-0.5 pr-2 align-top font-mono">
|
||||
{field.name}
|
||||
{field.path !== field.name && (
|
||||
<span className="block text-white/30">{field.path}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-0.5 pr-2 align-top">
|
||||
{field.type}
|
||||
{field.required && <span className="text-white/30">, povinný</span>}
|
||||
</td>
|
||||
<td className="min-w-0 py-0.5 align-top">
|
||||
{field.got}
|
||||
{field.preview !== '' && (
|
||||
<span className="block break-all font-mono text-white/40">
|
||||
{field.preview}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-white/35">
|
||||
Přijaté tělo{call.truncated && ' (useknuté)'}
|
||||
</p>
|
||||
<pre className="mt-1 max-h-64 overflow-auto rounded-lg bg-ink-950/60 p-2 text-xs text-white/60">
|
||||
{call.body === '' ? '(prázdné)' : call.body}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
|
||||
@@ -609,12 +609,28 @@ export interface AutomationFlow {
|
||||
steps: FlowStep[];
|
||||
}
|
||||
|
||||
/** Jedno volani webhooku, jak dopadlo. Telo se neuklada, jen duvod odmitnuti. */
|
||||
/** Jak dopadl jeden deklarovany parametr v jednom volani. */
|
||||
export interface WebhookCallField {
|
||||
name: string;
|
||||
path: string;
|
||||
required: boolean;
|
||||
type: string;
|
||||
/** Co na te ceste opravdu bylo: text, cislo, objekt, seznam, nepreslo. */
|
||||
got: string;
|
||||
preview: string;
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
/** Jedno volani webhooku, jak dopadlo, vcetne prijateho tela. */
|
||||
export interface WebhookCall {
|
||||
at: string;
|
||||
ok: boolean;
|
||||
problems: string[];
|
||||
runId: string | null;
|
||||
fields: WebhookCallField[];
|
||||
/** Cele prijate telo jako JSON. */
|
||||
body: string;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export interface AutomationDetail extends Automation {
|
||||
|
||||
Reference in New Issue
Block a user