From dd7db026e198458fb356fa92c70daab0f9473652 Mon Sep 17 00:00:00 2001 From: stroblme Date: Tue, 25 Aug 2026 22:33:09 +0200 Subject: [PATCH] Announce an acknowledged node failure on the bus Clearing a node's last error on the engine published nothing, so a second browser kept the marker until its next snapshot. One event carries the qualified node; the receiving client drops the marker without refetching. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013Gf7WaExcJ9bs3kfJXB3nK --- backend/fluksio/api/routes/flows.py | 9 ++++++ backend/tests/api/routes/test_flows.py | 30 +++++++++++++++++++ frontend/src/components/Flow/liveStore.ts | 4 +++ frontend/src/components/Flow/useFlowSocket.ts | 6 ++++ 4 files changed, 49 insertions(+) diff --git a/backend/fluksio/api/routes/flows.py b/backend/fluksio/api/routes/flows.py index 4f7600e..ec72429 100644 --- a/backend/fluksio/api/routes/flows.py +++ b/backend/fluksio/api/routes/flows.py @@ -779,6 +779,15 @@ def acknowledge_node_error( raise HTTPException( status_code=404, detail=f"No node named '{node_id}' in flow '{name}'" ) from None + # Another browser holds the same marker and has no reason to refetch, so it + # would keep showing a failure that is gone until its next snapshot. + event_bus.publish( + { + "type": "node_error_acknowledged", + "node": f"{name}.{node_id}", + "ts": time.time(), + } + ) return Message(message=f"Cleared the failure on '{node_id}'") diff --git a/backend/tests/api/routes/test_flows.py b/backend/tests/api/routes/test_flows.py index baa4fea..89e874a 100644 --- a/backend/tests/api/routes/test_flows.py +++ b/backend/tests/api/routes/test_flows.py @@ -1,10 +1,12 @@ from datetime import UTC, datetime +import pytest from fastapi.testclient import TestClient from sqlalchemy import func from sqlmodel import Session, select from fluksio.core.config import settings +from fluksio.flow.events import event_bus from fluksio.models import Run, RunArtifact, RunMetric, RunNode PREFIX = f"{settings.API_V1_STR}/flows" @@ -422,6 +424,34 @@ def test_a_node_that_raises_answers_with_its_error( client.delete(f"{PREFIX}/failing", headers=superuser_token_headers) +def test_acknowledging_a_failure_is_announced_on_the_bus( + client: TestClient, + superuser_token_headers: dict[str, str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Another browser holds the same marker; the event is what clears it.""" + saved = client.put( + f"{PREFIX}/acked", headers=superuser_token_headers, json=a_flow("acked") + ).json() + client.post( + f"{PREFIX}/acked/publish", + headers=superuser_token_headers, + json={"version": saved["definition"]["version"]}, + ) + + published: list[dict] = [] + monkeypatch.setattr(event_bus, "publish", published.append) + response = client.post( + f"{PREFIX}/acked/nodes/sensor/acknowledge", headers=superuser_token_headers + ) + + assert response.status_code == 200 + event = next(e for e in published if e["type"] == "node_error_acknowledged") + assert event["node"] == "acked.sensor" + + client.delete(f"{PREFIX}/acked", headers=superuser_token_headers) + + def test_renaming_a_flow_leaves_nothing_under_the_old_name( client: TestClient, superuser_token_headers: dict[str, str] ) -> None: diff --git a/frontend/src/components/Flow/liveStore.ts b/frontend/src/components/Flow/liveStore.ts index ec12a72..342dc67 100644 --- a/frontend/src/components/Flow/liveStore.ts +++ b/frontend/src/components/Flow/liveStore.ts @@ -167,6 +167,10 @@ export const liveStore = { getFailure(nodeId: string) { return failures.get(nodeId) }, + /** Drop the marker for a failure the engine has already been told about. */ + clearFailure(nodeId: string) { + if (failures.delete(nodeId)) notify(`failure:${nodeId}`) + }, /** * Dismiss a node's failure, here and on the engine. * diff --git a/frontend/src/components/Flow/useFlowSocket.ts b/frontend/src/components/Flow/useFlowSocket.ts index 6617a7c..d17044b 100644 --- a/frontend/src/components/Flow/useFlowSocket.ts +++ b/frontend/src/components/Flow/useFlowSocket.ts @@ -57,6 +57,7 @@ type FlowEvent = ts?: number } | { type: "node_status"; node: string; status: string; error?: string | null } + | { type: "node_error_acknowledged"; node: string; ts?: number } | ({ type: "node_log" } & LogLine) | { type: "flow_paused"; flow: string; paused: boolean } | { @@ -235,6 +236,11 @@ function connect() { error: message.error, }) break + case "node_error_acknowledged": + // Someone dismissed it, here or in another browser. The engine has + // already forgotten it, so drop the marker rather than posting back. + liveStore.clearFailure(message.node) + break case "node_log": liveStore.appendLog(message) break