Kontrakt webhooku: objekt jde vybrat a odmitnuti je videt

Parametr spoustece `data` byl deklarovany jako type string a povinny, zatimco
odesilatel ho posila jako objekt a v prvni zprave hovoru ho jeste nema. Kazde
volani proto skoncilo na 400 a automatizace hodinu nedelala nic.

Za tim byly tri veci, kazda sama o sobe malicherna:

- rucne pridany parametr byl vychozi povinny, zatimco parametr odvozeny
  z ukazkoveho tela nepovinny. Dve ruzna vychozi nastaveni pro tutez vec
  v jednom formulari. Nove je nepovinny i rucne pridany: povinny znamena
  "odmitni volani" a do toho nema nikdo spadnout omylem
- objekt a seznam neslo vybrat. declarableFieldTypes nabizel jen string, number,
  boolean a date, a TriggerConfig.tsx mel jeste treti kopii toho seznamu.
  Deklarovat data jako objekt tedy neslo, i kdyz matchesType objekt umi
  a operatorsByType pro nej ma operatory
- odmitnuti nebylo nikde videt. Skoncilo jako console.warn v logu kontejneru:
  zadna udalost, zadny beh, nic na detailu automatizace

Ten treti bod je ten podstatny. Chybu v kontraktu udela ten, kdo ho psal, ale
400 dostane odesilatel - a ten s tim nic nenadela, casto je to cizi sluzba,
ktera volani neopakuje. Majitel automatizace se nedozvi nic a v portalu vypada
vsechno v poradku.

Detail automatizace proto ukazuje poslednich deset volani: cas, jestli proslo
nebo ne, a u odmitnutych duvod. Telo se schvalne neuklada, duvod uz rika, co je
spatne, a drzet payloady by znamenalo mit v pameti kopie zakaznickych dat.
Seznam je v pameti, restart ho zahodi. Incident se z toho nezaklada
a upozorneni se neposila: staci radek, implementator se ozve sam.

Vzorova automatizace ma data opravene na object a nepovinne.

Do navrhu 25 jsou zapsana rozhodnuti z diskuze: "moje tickety" jsou tickety
prirazene mne, v helpdesku ty, ktere jsem zalozil ja, a helpdeskove pozadavky
vidi lide podle teze hierarchie jako tickety. Sekce 6 popisuje tuhle zmenu.

Overeno na bezici instanci s vlastnim DATA_DIR: telo s data jako objektem
projde, prvni zprava hovoru s data null projde, telo bez callSid se dal odmita,
a vsechna tri jsou videt v seznamu poslednich volani.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-09-02 10:14:48 +02:00
co-authored by Claude Opus 5
parent 72da07debe
commit e8bb4d6e54
8 changed files with 302 additions and 41 deletions
@@ -12,15 +12,19 @@ import { useState } from 'react';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/lib/cn';
import { fieldTypeLabels, newFieldId } from '@/lib/flow';
import type {
Service,
FieldType,
FlowTrigger,
ModelNode,
TriggerField,
import {
declarableFieldTypes,
type Service,
type FieldType,
type FlowTrigger,
type ModelNode,
type TriggerField,
} from '@/types/dashboard';
const fieldTypes: FieldType[] = ['string', 'number', 'boolean', 'date'];
// Nabidka typu se bere ze sdileneho seznamu. Driv tu byla vlastni kopie a ta
// se rozesla: objekt a seznam v ni chybely, takze `data` sla deklarovat jen
// jako text a kontrakt pak odmital kazde volani.
const fieldTypes: readonly FieldType[] = declarableFieldTypes;
/**
* Nastaveni spoustece: registrovana adresa webhooku a deklarace parametru,
@@ -101,8 +105,14 @@ function CustomFields({
editable: boolean;
onChange: (fields: TriggerField[]) => void;
}) {
/*
* Novy parametr je **nepovinny**. Povinny znamena "odmitni volani", a do toho
* nema nikdo spadnout omylem - zaskrtnout to jde vzdycky. Odvozeni parametru
* z ukazkoveho tela to tak delalo uz driv, rucni pridani ne, a ta nesrovnalost
* stala jedno odpoledne hledani, proc webhook prestal chodit.
*/
function addField() {
onChange([...fields, { id: newFieldId(), name: '', type: 'string', required: true }]);
onChange([...fields, { id: newFieldId(), name: '', type: 'string', required: false }]);
}
function updateField(id: string, patch: Partial<TriggerField>) {
@@ -28,6 +28,7 @@ import type {
AutomationFlow,
ServiceCatalog,
TriggerField,
WebhookCall,
} from '@/types/dashboard';
/** Kam se ma vlozit dalsi krok - null znamena, ze vyber neni otevreny. */
@@ -442,6 +443,10 @@ export default function AutomationDetail() {
</div>
)}
{flow.trigger?.serviceId === 'webhook' && (
<WebhookCalls calls={automation.data?.recentCalls ?? []} />
)}
<p className="flex gap-2 rounded-xl border border-ink-600/60 px-4 py-3 text-xs leading-relaxed text-white/40">
<Info className="mt-0.5 size-3.5 shrink-0" />
Prototyp: nastavení jednotlivých polí kroku (mapování dat, filtry) zatím
@@ -467,6 +472,55 @@ export default function AutomationDetail() {
);
}
/**
* Poslednich deset volani webhooku.
*
* 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.
*
* Telo se neuklada, jen duvod odmitnuti. Ten uz rika, co je spatne.
*/
function WebhookCalls({ calls }: { calls: WebhookCall[] }) {
return (
<div className="glass rounded-card p-5">
<h2 className="font-semibold text-white">Poslední volání</h2>
{calls.length === 0 ? (
<p className="mt-3 text-sm text-white/40">
Zatím nic nepřišlo. Objeví se tu deset posledních volání včetně těch,
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>
)}
</div>
</li>
))}
</ul>
)}
<p className="mt-3 text-xs text-white/35">
Drží se v paměti, restart je zahodí. Na historii jsou běhy.
</p>
</div>
);
}
function Row({ label, value }: { label: string; value: string }) {
return (
<div className="flex justify-between gap-3">
+22 -2
View File
@@ -506,8 +506,18 @@ export interface ServiceCatalog {
*/
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'object' | 'list';
/** Typy, ktere si uzivatel muze zvolit u vlastniho parametru spoustece. */
export const declarableFieldTypes = ['string', 'number', 'boolean', 'date'] as const;
/**
* Typy, ktere si uzivatel muze zvolit u vlastniho parametru spoustece.
* Objekt a seznam jsou mezi nimi kvuli telu, kde `data` prijde jako objekt.
*/
export const declarableFieldTypes = [
'string',
'number',
'boolean',
'date',
'object',
'list',
] as const;
export type ConditionOperator =
| 'eq'
@@ -599,10 +609,20 @@ export interface AutomationFlow {
steps: FlowStep[];
}
/** Jedno volani webhooku, jak dopadlo. Telo se neuklada, jen duvod odmitnuti. */
export interface WebhookCall {
at: string;
ok: boolean;
problems: string[];
runId: string | null;
}
export interface AutomationDetail extends Automation {
flow: AutomationFlow;
/** Model prichozich dat z ukazky u spoustece. Pocita server pri cteni. */
model: ModelNode[];
/** Poslednich deset volani webhooku, nejnovejsi prvni. Drzi se v pameti. */
recentCalls: WebhookCall[];
createdAt: string;
updatedAt: string;
}