diff --git a/coordinator/internal/transport/http/server_test.go b/coordinator/internal/transport/http/server_test.go
index 29bbd8a..2fd9836 100644
--- a/coordinator/internal/transport/http/server_test.go
+++ b/coordinator/internal/transport/http/server_test.go
@@ -472,7 +472,7 @@ func TestSimilaritySearchLifecyclePublishesFinalResult(t *testing.T) {
}
defer uiResponse.Body.Close()
uiBody, _ := io.ReadAll(uiResponse.Body)
- if uiResponse.StatusCode != http.StatusOK || !strings.Contains(string(uiBody), "Final result ready") {
+ if uiResponse.StatusCode != http.StatusOK || !strings.Contains(string(uiBody), "Final result ready") || !strings.Contains(string(uiBody), "Preview CSV") {
t.Fatalf("final UI = (%d, %q)", uiResponse.StatusCode, uiBody)
}
diff --git a/coordinator/internal/transport/http/templates/job.html b/coordinator/internal/transport/http/templates/job.html
index 63cfbce..0234f40 100644
--- a/coordinator/internal/transport/http/templates/job.html
+++ b/coordinator/internal/transport/http/templates/job.html
@@ -6,7 +6,7 @@
SciMesh pipeline
@@ -17,7 +17,7 @@
Pipeline stages
Each stage reflects coordinator state, not a simulated progress bar.
1TSV acceptedThe coordinator stored the source and created shard tasks.
2Shards execute{{.Completed}} of {{.Total}} candidate partitions are complete.
3Workers return CSVsWorkers upload a checked partial result for every completed shard.
4Global reductionThe coordinator waits until all shards are complete.
5Final CSVAvailable only after deterministic reduction succeeds.
- Run configuration
Allowlisted scientific parameters.
What is being computed?
{{range .Parameters}}
{{.Label}}{{.Value}}
{{else}}
No displayable parameters were supplied.
{{end}}
Result status
Safe operator guidance.
{{if .FinalResultAvailable}}Final result ready
The coordinator merged shard candidates with exact scores and stored the global top-k CSV.
{{range .Artifacts}}{{if and (eq .Kind "final_result") .Downloadable}}Download final CSV{{end}}{{end}}{{else if eq .Status "reducing"}}Merging completed shards
The final candidate heap is being ranked now. This page will update when the CSV is stored.
{{else if eq .Status "failed"}}Run needs attention
{{if .ErrorMessage}}{{.ErrorMessage}}{{else}}One or more shards could not produce a final result. Review the task table below.{{end}}
{{else}}Waiting for the final result
Partial CSVs are diagnostics. They become one global result only after every shard completes and reduction succeeds.
{{end}}
+ Run configuration
Allowlisted scientific parameters.
What is being computed?
{{range .Parameters}}
{{.Label}}{{.Value}}
{{else}}
No displayable parameters were supplied.
{{end}}
Result status
Safe operator guidance.
{{if .FinalResultAvailable}}Final result ready
The coordinator merged shard candidates with exact scores and stored the global top-k CSV.
{{range .Artifacts}}{{if and (eq .Kind "final_result") .Downloadable}}Preview CSVDownload final CSV{{end}}{{end}}{{else if eq .Status "reducing"}}Merging completed shards
The final candidate heap is being ranked now. This page will update when the CSV is stored.
{{else if eq .Status "failed"}}Run needs attention
{{if .ErrorMessage}}{{.ErrorMessage}}{{else}}One or more shards could not produce a final result. Review the task table below.{{end}}
{{else}}Waiting for the final result
Partial CSVs are diagnostics. They become one global result only after every shard completes and reduction succeeds.
{{end}}
Shard activity
Every task is one input partition. The table refreshes while work is in progress.
| Shard | State | Attempt | Worker / lease | Outcome |
{{range .Tasks}}| #{{.ChunkIndex}} | {{statusLabel .Status}} | {{.Attempt}} / {{.MaxAttempts}} | {{if .LeaseOwner}}{{.LeaseOwner}}{{if .LeaseExpiresAt}} lease until {{time .LeaseExpiresAt}}{{end}}{{else}}—{{end}} | {{if .ErrorCode}}{{taskErrorLabel .ErrorCode}} {{taskErrorHint .ErrorCode}}{{else if eq .Status "completed"}}Partial CSV uploaded{{else}}—{{end}} |
{{else}}| No shard tasks are present yet. |
{{end}}
@@ -30,7 +30,7 @@
const stage=(index,title,description,state)=>{const card=text('article',undefined,'stage stage-'+state);card.append(text('span',String(index),'index'),text('b',title),text('p',description));return card};
const renderStages=job=>{const holder=document.querySelector('#stages'),allDone=job.completed===job.total&&job.total>0,reducerFailed=job.status==='failed'&&job.error_code==='reducer_failed',shardFailed=job.status==='failed'&&!reducerFailed;holder.replaceChildren(stage(1,'TSV accepted','The coordinator stored the source and created shard tasks.','done'),stage(2,'Shards execute',job.completed+' of '+job.total+' candidate partitions are complete.',shardFailed?'failed':allDone?'done':(job.running||job.leased)?'active':'waiting'),stage(3,'Workers return CSVs',allDone?'Every completed shard has a coordinator-owned partial CSV.':(job.running||job.leased)?'Workers are actively claiming and processing partitions.':'Waiting for a worker to claim a shard.',shardFailed?'failed':allDone?'done':(job.running||job.leased)?'active':'waiting'),stage(4,'Global reduction',reducerFailed?'The coordinator could not safely reduce partial results.':job.status==='reducing'?'The coordinator is merging exact candidate scores.':job.status==='completed'?'The global top-k has been merged deterministically.':'Reduction begins only after every shard completes.',reducerFailed?'failed':job.status==='reducing'?'active':job.status==='completed'?'done':'waiting'),stage(5,'Final CSV',job.final_result_available?'The checksum-protected global result is ready.':'Available only after deterministic reduction succeeds.',job.final_result_available?'done':reducerFailed?'failed':'waiting'))};
const renderParameters=parameters=>{const holder=document.querySelector('#parameters');holder.replaceChildren();if(!parameters.length){holder.append(text('p','No displayable parameters were supplied.'));return}for(const parameter of parameters){const row=text('div',undefined,'parameter');row.append(text('span',parameter.label),text('code',parameter.value));holder.append(row)}};
- const renderResult=job=>{const card=document.querySelector('#result-card');card.className='run-note';card.replaceChildren();if(job.final_result_available){card.classList.add('result');card.append(text('h3','Final result ready'),text('p','The coordinator merged shard candidates with exact scores and stored the global top-k CSV.'));const final=(job.artifacts||[]).find(a=>a.kind==='final_result'&&a.downloadable);if(final){const link=text('a','Download final CSV','download');link.href='/ui/jobs/'+encodeURIComponent(job.id)+'/artifacts/'+encodeURIComponent(final.id);card.append(link)}}else if(job.status==='reducing'){card.append(text('h3','Merging completed shards'),text('p','The final candidate heap is being ranked now. This page will update when the CSV is stored.'))}else if(job.status==='failed'){card.classList.add('alert');card.append(text('h3','Run needs attention'),text('p',job.error_message||'One or more shards could not produce a final result. Review the task table below.'))}else{card.append(text('h3','Waiting for the final result'),text('p','Partial CSVs are diagnostics. They become one global result only after every shard completes and reduction succeeds.'))}};
+ const renderResult=job=>{const card=document.querySelector('#result-card');card.className='run-note';card.replaceChildren();if(job.final_result_available){card.classList.add('result');card.append(text('h3','Final result ready'),text('p','The coordinator merged shard candidates with exact scores and stored the global top-k CSV.'));const final=(job.artifacts||[]).find(a=>a.kind==='final_result'&&a.downloadable);if(final){const preview=text('a','Preview CSV','download');preview.href='/ui/jobs/'+encodeURIComponent(job.id)+'/artifacts/'+encodeURIComponent(final.id)+'/preview';const download=text('a','Download final CSV','download');download.href='/ui/jobs/'+encodeURIComponent(job.id)+'/artifacts/'+encodeURIComponent(final.id);card.append(preview,download)}}else if(job.status==='reducing'){card.append(text('h3','Merging completed shards'),text('p','The final candidate heap is being ranked now. This page will update when the CSV is stored.'))}else if(job.status==='failed'){card.classList.add('alert');card.append(text('h3','Run needs attention'),text('p',job.error_message||'One or more shards could not produce a final result. Review the task table below.'))}else{card.append(text('h3','Waiting for the final result'),text('p','Partial CSVs are diagnostics. They become one global result only after every shard completes and reduction succeeds.'))}};
const renderTasks=tasks=>{const holder=document.querySelector('#tasks');holder.replaceChildren();if(!tasks.length){const row=document.createElement('tr'),cell=text('td','No shard tasks are present yet.','empty');cell.colSpan=5;row.append(cell);holder.append(row);return}for(const task of tasks){const row=document.createElement('tr'),info=statusInfo[task.status]||[task.status,'waiting',''];row.append(text('td','#'+task.chunk_index));const state=text('td'),badge=text('span',info[0],'badge badge-'+info[1]);state.append(badge);row.append(state,text('td',task.attempt+' / '+task.max_attempts));const worker=text('td');if(task.lease_owner){worker.append(text('strong',task.lease_owner));if(task.lease_expires_at){worker.append(document.createElement('br'),text('small','lease until '+fmtTime(task.lease_expires_at),'muted'))}}else worker.append(text('span','—','muted'));row.append(worker);const outcome=text('td',undefined,'error');if(task.error_code){const explanation=taskError[task.error_code]||['Task needs attention','Check the worker terminal for the original error.'];outcome.append(text('strong',explanation[0]),document.createElement('br'),text('small',explanation[1]))}else if(task.status==='completed')outcome.append(text('span','Partial CSV uploaded','muted'));else outcome.append(text('span','—','muted'));row.append(outcome);holder.append(row)}};
const renderArtifacts=job=>{const holder=document.querySelector('#artifacts');holder.replaceChildren();const artifacts=job.artifacts||[];if(!artifacts.length){holder.append(text('div','Artifacts appear here as the coordinator stores input, shards, partial results, and the final CSV.','empty'));return}for(const artifact of artifacts){const card=text('article',undefined,'artifact'+(artifact.kind==='final_result'?' artifact-final':''));card.append(text('div',artifact.diagnostic?'Partial result · diagnostic':artifact.kind,'artifact-type'),text('strong',artifact.filename),text('span',fmtBytes(artifact.size_bytes),'muted'),text('code','SHA-256 '+artifact.sha256));if(artifact.downloadable){const preview=text('a','Preview CSV');preview.href='/ui/jobs/'+encodeURIComponent(job.id)+'/artifacts/'+encodeURIComponent(artifact.id)+'/preview';const download=text('a',artifact.kind==='final_result'?'Download final CSV':'Download CSV');download.href='/ui/jobs/'+encodeURIComponent(job.id)+'/artifacts/'+encodeURIComponent(artifact.id);card.append(preview,download)}holder.append(card)}};
const render=job=>{const info=statusInfo[job.status]||[job.status,'waiting','Status reported by the coordinator.'],active=(job.leased||0)+(job.running||0),done=(job.completed||0)+(job.failed||0)+(job.cancelled||0),badge=document.querySelector('#status');badge.textContent=info[0];badge.className='badge badge-'+info[1];document.querySelector('#hint').textContent=info[2];document.querySelector('#progress-bar').style.width=pct(job)+'%';document.querySelector('#progress').textContent=job.completed+' of '+job.total+' shards complete'+(job.failed?' · '+job.failed+' failed':'')+(job.cancelled?' · '+job.cancelled+' stopped':'');for(const [key,value] of Object.entries({total:job.total,completed:job.completed,pending:job.pending,active,failed:job.failed,cancelled:job.cancelled}))document.querySelector('#'+key).textContent=value;document.querySelector('#stop-wrap').classList.toggle('hidden',!(job.status==='pending'||job.status==='running'));document.querySelector('#task-caption').textContent=done+' of '+job.total+' task states are terminal. The table refreshes while work is in progress.';renderStages(job);renderParameters(job.parameters||[]);renderResult(job);renderTasks(job.tasks||[]);renderArtifacts(job)};