diff --git a/documentation/25-navrh-pristupny-portal.md b/documentation/25-navrh-pristupny-portal.md index 0e455c6..2b03dae 100644 --- a/documentation/25-navrh-pristupny-portal.md +++ b/documentation/25-navrh-pristupny-portal.md @@ -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 diff --git a/documentation/99-zmeny.md b/documentation/99-zmeny.md index c163602..a20ee9a 100644 --- a/documentation/99-zmeny.md +++ b/documentation/99-zmeny.md @@ -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`, diff --git a/src/data/automationStore.ts b/src/data/automationStore.ts index 5a47ba8..168336e 100644 --- a/src/data/automationStore.ts +++ b/src/data/automationStore.ts @@ -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. */ diff --git a/src/routes/webhook.ts b/src/routes/webhook.ts index 84f1d6a..c4cb4de 100644 --- a/src/routes/webhook.ts +++ b/src/routes/webhook.ts @@ -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; - 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, -): { values: Record; problems: string[] } { +): { values: Record; problems: string[]; report: WebhookCallField[] } { const values: Record = {}; 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. */ diff --git a/web/src/pages/dashboard/AutomationDetail.tsx b/web/src/pages/dashboard/AutomationDetail.tsx index e43669a..1b56479 100644 --- a/web/src/pages/dashboard/AutomationDetail.tsx +++ b/web/src/pages/dashboard/AutomationDetail.tsx @@ -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(null); + return (

Poslední volání

@@ -493,24 +507,88 @@ function WebhookCalls({ calls }: { calls: WebhookCall[] }) { která jsme odmítli.

) : ( -
    - {calls.map((call) => ( -
  • - {call.ok ? ( - - ) : ( - - )} -
    - {formatDateTime(call.at)} - {call.problems.length > 0 && ( -

    - {call.problems.join(' ')} -

    +
      + {calls.map((call) => { + const open = openAt === call.at; + return ( +
    • + + + {open && ( +
      + {call.fields.length > 0 && ( + + + + + + + + + + {call.fields.map((field) => ( + + + + + + ))} + +
      ParametrČekámePřišlo
      + {field.name} + {field.path !== field.name && ( + {field.path} + )} + + {field.type} + {field.required && , povinný} + + {field.got} + {field.preview !== '' && ( + + {field.preview} + + )} +
      + )} + +
      +

      + Přijaté tělo{call.truncated && ' (useknuté)'} +

      +
      +                        {call.body === '' ? '(prázdné)' : call.body}
      +                      
      +
      +
      )} -
    -
  • - ))} + + ); + })}
)} diff --git a/web/src/types/dashboard.ts b/web/src/types/dashboard.ts index 8c823f1..1525401 100644 --- a/web/src/types/dashboard.ts +++ b/web/src/types/dashboard.ts @@ -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 {