diff --git a/frontend/src/components/Dashboard/DashboardView.tsx b/frontend/src/components/Dashboard/DashboardView.tsx
index 0157d19..d92c007 100644
--- a/frontend/src/components/Dashboard/DashboardView.tsx
+++ b/frontend/src/components/Dashboard/DashboardView.tsx
@@ -9,13 +9,7 @@ import { cn } from "@/lib/utils"
import "./dashboard.css"
import { WidgetBody, WidgetFrame, widgetIssue } from "./widgets"
-/**
- * A dashboard, plus the settings the generated client does not carry yet.
- *
- * `columns` is stored by the backend but the SDK is regenerated on its own
- * schedule, so it is widened here rather than waited for.
- */
-export type Dashboard = DashboardDef_Output & { columns?: number }
+export type Dashboard = DashboardDef_Output
/** How many columns a wall panel is cut into, if the document does not say. */
export const DEFAULT_COLUMNS = 12
diff --git a/frontend/src/components/Flow/FlowDock.tsx b/frontend/src/components/Flow/FlowDock.tsx
index 59d567a..3f72d90 100644
--- a/frontend/src/components/Flow/FlowDock.tsx
+++ b/frontend/src/components/Flow/FlowDock.tsx
@@ -9,6 +9,7 @@ import {
Play,
PlayCircle,
Plus,
+ StepForward,
WifiOff,
ZoomIn,
ZoomOut,
@@ -62,6 +63,8 @@ export function FlowDock({
onAddNode,
onRun,
onTogglePause,
+ onStep,
+ stepping,
onFocusNode,
onEditFlow,
onPublish,
@@ -79,6 +82,8 @@ export function FlowDock({
onAddNode: () => void
onRun: () => void
onTogglePause: () => void
+ onStep: () => void
+ stepping: boolean
onFocusNode: (nodeId: string) => void
onEditFlow: () => void
onPublish: () => void
@@ -214,6 +219,25 @@ export function FlowDock({
+ {paused && enabled ? (
+
+
+
+
+ Let one held-back item through
+
+ ) : null}
+
diff --git a/frontend/src/components/Flow/FlowEditor.tsx b/frontend/src/components/Flow/FlowEditor.tsx
index ff723fd..9edea38 100644
--- a/frontend/src/components/Flow/FlowEditor.tsx
+++ b/frontend/src/components/Flow/FlowEditor.tsx
@@ -211,7 +211,7 @@ function FlowEditorInner({
}) {
const navigate = useNavigate()
const queryClient = useQueryClient()
- const { showErrorToast } = useCustomToast()
+ const { showErrorToast, showSuccessToast } = useCustomToast()
const { screenToFlowPosition, fitView } = useReactFlow()
const updateNodeInternals = useUpdateNodeInternals()
@@ -507,6 +507,13 @@ function FlowEditorInner({
onError: () => showErrorToast("The flow could not be paused."),
})
+ const stepMutation = useMutation({
+ mutationFn: () => FlowsService.stepFlow({ name: flowName }),
+ // Says which node ran, or that nothing was held back — both are answers.
+ onSuccess: (result) => showSuccessToast(result.message),
+ onError: () => showErrorToast("The flow could not be stepped."),
+ })
+
const deleteMutation = useMutation({
mutationFn: () => FlowsService.deleteFlow({ name: flowName }),
onSuccess: () => {
@@ -970,6 +977,8 @@ function FlowEditorInner({
runMutation.mutate()
}}
onTogglePause={() => pauseMutation.mutate(!paused)}
+ onStep={() => stepMutation.mutate()}
+ stepping={stepMutation.isPending}
onFocusNode={focusNode}
/>