From ac9b9214016ec817c74ecbdcf24e0382bec67cd3 Mon Sep 17 00:00:00 2001 From: Emil Date: Thu, 23 Jul 2026 22:41:14 +0300 Subject: [PATCH] Clarify operator UI workflow --- .../internal/transport/http/server_test.go | 2 +- .../transport/http/templates/dashboard.html | 58 ++++++++- .../transport/http/templates/job.html | 43 ++++++- .../transport/http/templates/new-job.html | 71 ++++++++++- coordinator/internal/transport/http/ui.go | 112 +++++++++++++++++- .../internal/transport/http/ui_test.go | 35 ++++++ 6 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 coordinator/internal/transport/http/ui_test.go diff --git a/coordinator/internal/transport/http/server_test.go b/coordinator/internal/transport/http/server_test.go index 05c181e..6199c51 100644 --- a/coordinator/internal/transport/http/server_test.go +++ b/coordinator/internal/transport/http/server_test.go @@ -140,7 +140,7 @@ func TestUIRequiresDistinctCredentialAndRendersDashboard(t *testing.T) { t.Fatalf("UI status: %d", resp.StatusCode) } body, _ := io.ReadAll(resp.Body) - if !strings.Contains(string(body), "SciMesh operator UI") { + if !strings.Contains(string(body), "Панель оператора SciMesh") { t.Errorf("dashboard body missing title") } } diff --git a/coordinator/internal/transport/http/templates/dashboard.html b/coordinator/internal/transport/http/templates/dashboard.html index 2f620b6..695c3c8 100644 --- a/coordinator/internal/transport/http/templates/dashboard.html +++ b/coordinator/internal/transport/http/templates/dashboard.html @@ -1 +1,57 @@ -{{define "dashboard.html"}}SciMesh

SciMesh operator UI

Pipeline diagnostics Partial artifacts are not final scientific results until a reducer is implemented.

New similarity-search check

Recent jobs

{{range .Jobs}}{{else}}{{end}}
IDWorkloadStatusProgressCreated
{{.ID}}{{.Workload}}{{.Status}}{{.Completed}} / {{.Total}}{{time .CreatedAt}}
No jobs yet.

Workers

{{range .Workers}}{{else}}{{end}}
NameStatusCapabilitiesLast heartbeat
{{.Name}}{{.Status}}{{range .Capabilities}}{{.}} {{end}}{{time .LastHeartbeatAt}}
No workers registered.
{{end}} +{{define "dashboard.html"}} + + + + + + SciMesh — панель оператора + + + +
+
+
+

Локальный coordinator

+

Панель оператора SciMesh

+

Здесь видно, где находится вычисление и что нужно сделать дальше.

+
+ Запустить проверку +
+ +
+ Текущий этап — диагностика shard-задач. + Worker загружает частичные CSV в coordinator. Пока reducer не реализован, эти файлы не являются единым финальным научным результатом. +
1. Загрузить TSVCoordinator делит файл на shard-задачи.
2. Дождаться workerWorker берёт shard, вычисляет сходство и возвращает CSV.
3. Проверить артефактыСкачайте частичный результат из карточки задачи.
+
+ +

Последние задания

+
+ + + {{range .Jobs}} + + + + + + + {{else}} + + {{end}} +
ВычислениеСостояниеПрогрессСоздано
{{workloadLabel .Workload}}
Открыть детали задачи
{{statusLabel .Status}}
{{statusHint .Status}}
{{.Completed}} / {{.Total}} завершено{{if gt .Failed 0}} · ошибок: {{.Failed}}{{end}}
{{time .CreatedAt}}
Заданий пока нет.
Нажмите «Запустить проверку», загрузите небольшой TSV и оставьте worker запущенным.
+
+ +

Workers

+
+ {{range .Workers}} +
{{.Name}}
{{.ID}}
{{workerStatusLabel .Status}}
{{range .Capabilities}}{{.}} {{end}}
Последний сигнал
{{time .LastHeartbeatAt}}
+ {{else}} +
Worker ещё не зарегистрирован.
Запустите scimesh-worker с URL coordinator и worker token.
+ {{end}} +
+
+ + +{{end}} diff --git a/coordinator/internal/transport/http/templates/job.html b/coordinator/internal/transport/http/templates/job.html index 44fec9a..1b0d2d1 100644 --- a/coordinator/internal/transport/http/templates/job.html +++ b/coordinator/internal/transport/http/templates/job.html @@ -1 +1,42 @@ -{{define "job.html"}}SciMesh job

← Dashboard

Job {{.ID}}

Pipeline check: downloaded partial_result files are diagnostic, not final reduced outputs.

Workload
{{.Workload}}
Status
{{.Status}}
Progress
{{.Completed}} / {{.Total}} completed; {{.Failed}} failed

Tasks

{{range .Tasks}}{{end}}
ChunkStatusAttemptLeaseError
{{.ChunkIndex}}{{.Status}}{{.Attempt}} / {{.MaxAttempts}}{{.LeaseOwner}} {{if .LeaseExpiresAt}}until {{time .LeaseExpiresAt}}{{end}}{{.ErrorCode}} {{.ErrorMessage}}

Coordinator artifacts

{{range .Artifacts}}{{end}}
KindFileSizeChecksum
{{.Kind}}{{if .Diagnostic}} (diagnostic){{end}}{{.Filename}}{{.SizeBytes}}{{.SHA256}}{{if .Downloadable}}Download{{end}}
{{end}} +{{define "job.html"}} + + + + + + Задача SciMesh + + + +
+ ← К списку заданий +

{{workloadLabel .Workload}}

+

Ход выполнения

+ +
+
{{statusLabel .Status}}

{{statusHint .Status}}

Сводка обновляется автоматически каждые 2 секунды.
+
+

{{.Completed}} из {{.Total}} задач завершено

+
{{.Total}}всего shard
{{.Completed}}завершено
{{.Pending}}ожидает
{{add .Leased .Running}}у worker
{{.Failed}}ошибок
+
Технические сведения

Job ID: {{.ID}}
Вычисление: {{.Workload}}
Создано: {{time .CreatedAt}}

+
+ +
Что можно скачать сейчас?
Файлы partial_result — результат отдельных shards. Они полезны для проверки pipeline, но ещё не являются объединённым финальным CSV: reducer пока не реализован.
+ +

Shard-задачи

+

Если есть ошибки, в этой таблице видно код и сообщение. Подробные строки обновляются после обычного обновления страницы.

+
{{range .Tasks}}{{else}}{{end}}
ShardСостояниеПопыткаWorker / leaseОшибка
#{{.ChunkIndex}}{{statusLabel .Status}}{{.Attempt}} / {{.MaxAttempts}}{{if .LeaseOwner}}{{.LeaseOwner}}{{if .LeaseExpiresAt}}
до {{time .LeaseExpiresAt}}{{end}}{{else}}{{end}}
{{if .ErrorCode}}{{.ErrorCode}}
{{.ErrorMessage}}{{else}}{{end}}
Задачи пока не появились.
+ +

Артефакты coordinator

+
{{range .Artifacts}}{{else}}{{end}}
ТипФайлРазмерПроверка целостности
{{if .Diagnostic}}Частичный результат
диагностический{{else}}{{.Kind}}{{end}}
{{.Filename}}{{bytes .SizeBytes}}{{.SHA256}}{{if .Downloadable}}Скачать CSV{{else}}Недоступно{{end}}
Артефактов ещё нет: worker загрузит CSV после выполнения shard.
+
+ + + +{{end}} diff --git a/coordinator/internal/transport/http/templates/new-job.html b/coordinator/internal/transport/http/templates/new-job.html index 0409bbf..0a7cce7 100644 --- a/coordinator/internal/transport/http/templates/new-job.html +++ b/coordinator/internal/transport/http/templates/new-job.html @@ -1 +1,70 @@ -{{define "new-job.html"}}New SciMesh run

← Dashboard

New similarity-search pipeline check

This creates a diagnostic shard job. Use query_smiles; global multi-shard reduction is not available yet.

{{end}} +{{define "new-job.html"}} + + + + + + Создать проверку — SciMesh + + + +
+ ← К списку заданий +

Пошаговый запуск

+

Поиск похожих молекул

+

Создаёт диагностическую задачу similarity-search: worker найдёт top-k молекул, похожих на заданный SMILES.

+ +
+ Перед запуском +
  • Оставьте хотя бы один scimesh-worker запущенным.
  • Для ручной проверки используйте небольшой TSV.
  • «Строк в shard» не ограничивает размер файла. Он делит весь загруженный файл на задачи: большой ChEMBL TSV при 1000 строках создаст тысячи задач.
+
+ +
+ + +

Ожидаются столбцы chembl_id и canonical_smiles.

+ + + +

Пример CCO — этанол. Для гефитиниба используйте его SMILES или локальный CLI с --query-id.

+ + + +

Top-k для каждого shard. Это пока не глобальный top-k всего датасета.

+ + + +

Меньше строк — больше задач и более наглядный прогресс; больше строк — меньше задач, но дольше каждая.

+ + + + +
+
+ + + +{{end}} diff --git a/coordinator/internal/transport/http/ui.go b/coordinator/internal/transport/http/ui.go index dc44edb..3074e1a 100644 --- a/coordinator/internal/transport/http/ui.go +++ b/coordinator/internal/transport/http/ui.go @@ -2,6 +2,7 @@ package http import ( "embed" + "fmt" "html/template" "io" "mime" @@ -18,9 +19,118 @@ import ( var uiFiles embed.FS var uiTemplates = template.Must(template.New("ui").Funcs(template.FuncMap{ - "time": func(t time.Time) string { return t.UTC().Format(time.RFC3339) }, + "time": formatUITime, + "statusLabel": uiStatusLabel, + "statusHint": uiStatusHint, + "statusClass": uiStatusClass, + "workerStatusLabel": uiWorkerStatusLabel, + "workloadLabel": uiWorkloadLabel, + "progressPercent": uiProgressPercent, + "bytes": uiBytes, + "add": func(a, b int) int { return a + b }, }).ParseFS(uiFiles, "templates/*.html")) +func formatUITime(t time.Time) string { + if t.IsZero() { + return "—" + } + return t.UTC().Format("02.01.2006 15:04 UTC") +} + +func uiStatusLabel(status string) string { + switch status { + case "pending": + return "Ожидает worker" + case "leased": + return "Выдана worker" + case "running": + return "Выполняется" + case "completed": + return "Задачи завершены" + case "failed": + return "Требует внимания" + default: + return status + } +} + +func uiStatusHint(status string) string { + switch status { + case "pending": + return "Ожидает свободный worker с нужной возможностью." + case "leased": + return "Worker уже забрал задачу и должен скоро начать вычисление." + case "running": + return "Worker читает shard, считает fingerprints и загружает результат через coordinator." + case "completed": + return "Все shard-задачи завершены. Доступные ниже файлы пока являются частичными результатами." + case "failed": + return "Одна или несколько shard-задач завершились ошибкой. Откройте список задач ниже." + default: + return "Статус получен от coordinator." + } +} + +func uiStatusClass(status string) string { + switch status { + case "completed": + return "success" + case "failed": + return "danger" + case "running", "leased": + return "active" + default: + return "waiting" + } +} + +func uiWorkerStatusLabel(status string) string { + switch status { + case "online": + return "Доступен" + case "offline": + return "Нет связи" + default: + return status + } +} + +func uiWorkloadLabel(workload string) string { + switch workload { + case "similarity-search", "similarity_search": + return "Поиск похожих молекул" + case "similarity-graph", "similarity_graph": + return "Граф молекулярного сходства" + default: + return workload + } +} + +func uiProgressPercent(completed, failed, total int) int { + if total <= 0 { + return 0 + } + percent := (completed + failed) * 100 / total + if percent > 100 { + return 100 + } + return percent +} + +func uiBytes(n int64) string { + const kib = 1024 + if n < kib { + return fmt.Sprintf("%d Б", n) + } + if n < kib*kib { + return fmt.Sprintf("%.1f КиБ", float64(n)/kib) + } + if n < kib*kib*kib { + return fmt.Sprintf("%.1f МиБ", float64(n)/(kib*kib)) + } + return fmt.Sprintf("%.1f ГиБ", float64(n)/(kib*kib*kib)) +} + func (s *Server) renderUI(w http.ResponseWriter, name string, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") diff --git a/coordinator/internal/transport/http/ui_test.go b/coordinator/internal/transport/http/ui_test.go new file mode 100644 index 0000000..993036d --- /dev/null +++ b/coordinator/internal/transport/http/ui_test.go @@ -0,0 +1,35 @@ +package http + +import "testing" + +func TestUIStatusPresentation(t *testing.T) { + tests := []struct { + status string + label string + class string + }{ + {"pending", "Ожидает worker", "waiting"}, + {"running", "Выполняется", "active"}, + {"completed", "Задачи завершены", "success"}, + {"failed", "Требует внимания", "danger"}, + } + for _, test := range tests { + t.Run(test.status, func(t *testing.T) { + if got := uiStatusLabel(test.status); got != test.label { + t.Errorf("label = %q, want %q", got, test.label) + } + if got := uiStatusClass(test.status); got != test.class { + t.Errorf("class = %q, want %q", got, test.class) + } + }) + } +} + +func TestUIProgressPercent(t *testing.T) { + if got := uiProgressPercent(3, 1, 8); got != 50 { + t.Errorf("progress = %d, want 50", got) + } + if got := uiProgressPercent(1, 1, 0); got != 0 { + t.Errorf("empty progress = %d, want 0", got) + } +}