From 6bac7dad3ca174b2f63b7b02d3ce3c1ef2a81cdf Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 23 Jul 2026 22:59:51 +0300 Subject: [PATCH] Clarify worker failures in operator UI --- .../transport/http/templates/job.html | 2 +- coordinator/internal/transport/http/ui.go | 39 +++++++++++++++++++ .../internal/transport/http/ui_test.go | 9 +++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/coordinator/internal/transport/http/templates/job.html b/coordinator/internal/transport/http/templates/job.html index 3d744de..3ce8e6b 100644 --- a/coordinator/internal/transport/http/templates/job.html +++ b/coordinator/internal/transport/http/templates/job.html @@ -15,7 +15,7 @@
{{statusLabel .Status}}

{{statusHint .Status}}

Summary refreshes automatically every two seconds.

{{.Completed}} of {{.Total}} tasks complete

{{.Total}}total shards
{{.Completed}}complete
{{.Pending}}waiting
{{add .Leased .Running}}with workers
{{.Failed}}failed
Technical details

Job ID: {{.ID}}
Workload: {{.Workload}}
Created: {{time .CreatedAt}}

What can be downloaded now?
partial_result files come from individual shards. They are useful for checking the pipeline, but are not a merged final CSV because the reducer is not implemented yet.

Shard tasks

If a task fails, its code and message appear here. Refresh the page to update the detailed rows.

-
{{range .Tasks}}{{else}}{{end}}
ShardStateAttemptWorker / leaseError
#{{.ChunkIndex}}{{statusLabel .Status}}{{.Attempt}} / {{.MaxAttempts}}{{if .LeaseOwner}}{{.LeaseOwner}}{{if .LeaseExpiresAt}}
until {{time .LeaseExpiresAt}}{{end}}{{else}}{{end}}
{{if .ErrorCode}}{{.ErrorCode}}
{{.ErrorMessage}}{{else}}{{end}}
No tasks have appeared yet.
+
{{range .Tasks}}{{else}}{{end}}
ShardStateAttemptWorker / leaseError
#{{.ChunkIndex}}{{statusLabel .Status}}{{.Attempt}} / {{.MaxAttempts}}{{if .LeaseOwner}}{{.LeaseOwner}}{{if .LeaseExpiresAt}}
until {{time .LeaseExpiresAt}}{{end}}{{else}}{{end}}
{{if .ErrorCode}}{{taskErrorLabel .ErrorCode}}
{{taskErrorHint .ErrorCode}}{{else}}{{end}}
No tasks have appeared yet.

Coordinator artifacts

{{range .Artifacts}}{{else}}{{end}}
TypeFileSizeIntegrity check
{{if .Diagnostic}}Partial result
diagnostic{{else}}{{.Kind}}{{end}}
{{.Filename}}{{bytes .SizeBytes}}{{.SHA256}}{{if .Downloadable}}Download CSV{{else}}Unavailable{{end}}
No artifacts yet. The worker uploads a CSV after it completes a shard.
diff --git a/coordinator/internal/transport/http/ui.go b/coordinator/internal/transport/http/ui.go index e483ea8..f7d233e 100644 --- a/coordinator/internal/transport/http/ui.go +++ b/coordinator/internal/transport/http/ui.go @@ -23,6 +23,8 @@ var uiTemplates = template.Must(template.New("ui").Funcs(template.FuncMap{ "statusLabel": uiStatusLabel, "statusHint": uiStatusHint, "statusClass": uiStatusClass, + "taskErrorLabel": uiTaskErrorLabel, + "taskErrorHint": uiTaskErrorHint, "workerStatusLabel": uiWorkerStatusLabel, "workloadLabel": uiWorkloadLabel, "progressPercent": uiProgressPercent, @@ -95,6 +97,43 @@ func uiWorkerStatusLabel(status string) string { } } +// uiTaskErrorLabel deliberately maps worker implementation errors to an +// operator-facing diagnosis. Raw subprocess commands and local paths belong in +// the worker terminal, not in the web UI. +func uiTaskErrorLabel(errorCode string) string { + switch errorCode { + case "CalledProcessError": + return "Local calculation failed" + case "ValueError": + return "Task input could not be processed" + case "CoordinatorTransientError": + return "Coordinator connection was interrupted" + case "CoordinatorConflictError": + return "Worker lease was no longer valid" + case "FileNotFoundError": + return "Local task file is missing" + default: + return errorCode + } +} + +func uiTaskErrorHint(errorCode string) string { + switch errorCode { + case "CalledProcessError": + return "The local SciMesh command stopped before it could upload a result. Check the worker terminal for the original error." + case "ValueError": + return "The coordinator task or its downloaded input did not meet the worker validation rules." + case "CoordinatorTransientError": + return "The worker will retry after the coordinator connection is available again." + case "CoordinatorConflictError": + return "Another worker or a lease timeout changed this task before completion." + case "FileNotFoundError": + return "The worker could not find one of its local task files. Restart it with an absolute --work-dir." + default: + return "Check the worker terminal for the original error details." + } +} + func uiWorkloadLabel(workload string) string { switch workload { case "similarity-search", "similarity_search": diff --git a/coordinator/internal/transport/http/ui_test.go b/coordinator/internal/transport/http/ui_test.go index cb56a88..2d3b903 100644 --- a/coordinator/internal/transport/http/ui_test.go +++ b/coordinator/internal/transport/http/ui_test.go @@ -33,3 +33,12 @@ func TestUIProgressPercent(t *testing.T) { t.Errorf("empty progress = %d, want 0", got) } } + +func TestUITaskErrorPresentationDoesNotExposeCommand(t *testing.T) { + if got := uiTaskErrorLabel("CalledProcessError"); got != "Local calculation failed" { + t.Errorf("error label = %q", got) + } + if got := uiTaskErrorHint("CalledProcessError"); got == "" { + t.Error("error hint must explain the failure") + } +}