Let a node's failure outlive the run that followed it
A node's error cleared the moment it ran again, so a failure that genuinely fired an alert could leave no trace on the canvas by the time anyone looked. The engine records it now — on the node's status, so it survives a reload and every client agrees — and reading the traceback is what clears it. The seam is the event bus, which is where every failing path already meets: a queued live run, an explicit run, a preview, and a single triggered node all publish `node_error`, while the controller's own observer would have seen only one of them. That was half the confusion. The other half: clicking a failed neuron on Home often landed on a flow where everything looked fine. Nodes merge into one neuron by instance key — every InfluxDB node pointing at the same bucket is one neuron — and the click went to whichever flow contributed a member first, not the one that failed. It now goes to the failing member and selects it, and the canvas marks a failing node rather than leaving it to the dot alone. The inject node emitted one payload to every port it declared, whatever their types, so an inject on a bool port carrying the text "true" raised at publish time. Each port gets its own field now, typed and parsed by that port's dtype, and remembers what it last sent. A port that is renamed carries its value with it; one that is removed takes its value with it. An inject written before this keeps emitting exactly what it did. The derived-cron chip also appeared on the delay node, where `interval` is a rate limit and a schedule derived from it means nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
This commit is contained in:
@@ -32,7 +32,12 @@ import { useIsMobile } from "@/hooks/useMobile"
|
||||
import { duration } from "@/lib/motion"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { portOf } from "./deriveEdges"
|
||||
import { useNodeEmits, useNodeStatus } from "./liveStore"
|
||||
import {
|
||||
liveStore,
|
||||
useNodeEmits,
|
||||
useNodeFailure,
|
||||
useNodeStatus,
|
||||
} from "./liveStore"
|
||||
|
||||
const NODE_ICONS = {
|
||||
python: Code2,
|
||||
@@ -115,8 +120,11 @@ function PortHandles({
|
||||
function FlowNodeComponent({ data, selected }: NodeProps) {
|
||||
const { definition, flow, typeLabel, isPlugin, issueText, onShowLogs } =
|
||||
data as FlowNodeData
|
||||
const live = useNodeStatus(`${flow}.${definition.id}`)
|
||||
const emits = useNodeEmits(`${flow}.${definition.id}`)
|
||||
const nodeId = `${flow}.${definition.id}`
|
||||
const live = useNodeStatus(nodeId)
|
||||
const emits = useNodeEmits(nodeId)
|
||||
// What it last failed with, which outlives the run that failed.
|
||||
const failure = useNodeFailure(nodeId)
|
||||
// The graph runs top to bottom on a phone, so the ports have to face that
|
||||
// way too — see DESIGN-GUIDELINES.md → Responsive.
|
||||
const vertical = useIsMobile()
|
||||
@@ -154,6 +162,13 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
|
||||
const status = problem ? "error" : live?.status
|
||||
const running = status === "running"
|
||||
const style = STATUS_STYLES[status as keyof typeof STATUS_STYLES]
|
||||
// Failed at some point, fine as of the last run. It gets no dot and no
|
||||
// border — those tell the truth about the last run — only the traceback
|
||||
// button, which says in words when it was.
|
||||
const failedEarlier = !problem && Boolean(failure)
|
||||
const failedAt = failure
|
||||
? new Date(failure.ts * 1000).toLocaleTimeString()
|
||||
: ""
|
||||
|
||||
return (
|
||||
// The pulse ring measures itself from here rather than from the card, so
|
||||
@@ -172,7 +187,12 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
"relative min-w-[168px] max-w-[220px] rounded-lg border border-border bg-card px-3 py-2.5 shadow-e1 transition-shadow",
|
||||
"flow-node-card relative min-w-[168px] max-w-[220px] rounded-lg border border-border bg-card px-3 py-2.5 shadow-e1 transition-shadow",
|
||||
// Whatever is wrong right now is on the card as well as on the dot:
|
||||
// a click through from the brain has to land on something visible.
|
||||
// Selection wins the border when it is both — the dot is the status
|
||||
// channel, and it is still red underneath.
|
||||
problem && "border-destructive",
|
||||
selected && "border-primary shadow-e2",
|
||||
)}
|
||||
>
|
||||
@@ -220,7 +240,7 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
{status === "error" && onShowLogs ? (
|
||||
{(status === "error" || failedEarlier) && onShowLogs ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -228,18 +248,36 @@ function FlowNodeComponent({ data, selected }: NodeProps) {
|
||||
size="icon-sm"
|
||||
// `nodrag` keeps xyflow from reading the press as a drag; the
|
||||
// click itself is stopped so the node panel stays closed.
|
||||
className="nodrag nopan -my-1 size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
||||
aria-label="Show what this node printed"
|
||||
className={cn(
|
||||
"nodrag nopan -my-1 size-6 shrink-0 hover:text-destructive",
|
||||
// Red while it is the only thing left saying so, and named
|
||||
// in words beside it: colour never carries a status alone.
|
||||
failedEarlier
|
||||
? "text-destructive"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
aria-label={
|
||||
failedEarlier
|
||||
? `Failed at ${failedAt} — show the traceback`
|
||||
: "Show what this node printed"
|
||||
}
|
||||
data-testid="node-traceback"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onShowLogs(definition.id)
|
||||
// Reading it is what dismisses it: nothing else does, and a
|
||||
// marker that never goes away stops meaning anything.
|
||||
liveStore.acknowledgeFailure(nodeId)
|
||||
}}
|
||||
>
|
||||
<Bug />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Show the traceback</TooltipContent>
|
||||
<TooltipContent>
|
||||
{failedEarlier
|
||||
? `Failed at ${failedAt} — show the traceback`
|
||||
: "Show the traceback"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user