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.
- | Shard | State | Attempt | Worker / lease | Error |
{{range .Tasks}}| #{{.ChunkIndex}} | {{statusLabel .Status}} | {{.Attempt}} / {{.MaxAttempts}} | {{if .LeaseOwner}}{{.LeaseOwner}}{{if .LeaseExpiresAt}} until {{time .LeaseExpiresAt}}{{end}}{{else}}—{{end}} | {{if .ErrorCode}}{{.ErrorCode}} {{.ErrorMessage}}{{else}}—{{end}} |
{{else}}| No tasks have appeared yet. |
{{end}}
+ | Shard | State | Attempt | Worker / lease | Error |
{{range .Tasks}}| #{{.ChunkIndex}} | {{statusLabel .Status}} | {{.Attempt}} / {{.MaxAttempts}} | {{if .LeaseOwner}}{{.LeaseOwner}}{{if .LeaseExpiresAt}} until {{time .LeaseExpiresAt}}{{end}}{{else}}—{{end}} | {{if .ErrorCode}}{{taskErrorLabel .ErrorCode}} {{taskErrorHint .ErrorCode}}{{else}}—{{end}} |
{{else}}| No tasks have appeared yet. |
{{end}}
Coordinator artifacts
| Type | File | Size | Integrity check | |
{{range .Artifacts}}{{if .Diagnostic}}Partial result diagnostic{{else}}{{.Kind}}{{end}} | {{.Filename}} | {{bytes .SizeBytes}} | {{.SHA256}} | {{if .Downloadable}}Download CSV{{else}}Unavailable{{end}} |
{{else}}| No artifacts yet. The worker uploads a CSV after it completes a shard. |
{{end}}
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")
+ }
+}