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"}}
+
+
+
+ Текущий этап — диагностика shard-задач.
+ Worker загружает частичные CSV в coordinator. Пока reducer не реализован, эти файлы не являются единым финальным научным результатом.
+ 1. Загрузить TSVCoordinator делит файл на shard-задачи.
2. Дождаться workerWorker берёт shard, вычисляет сходство и возвращает CSV.
3. Проверить артефактыСкачайте частичный результат из карточки задачи.
+
+
+ Последние задания
+
+
+ | Вычисление | Состояние | Прогресс | Создано |
+ {{range .Jobs}}
+
+ {{workloadLabel .Workload}} Открыть детали задачи |
+ {{statusLabel .Status}} {{statusHint .Status}} |
+ {{.Completed}} / {{.Total}} завершено{{if gt .Failed 0}} · ошибок: {{.Failed}}{{end}}
|
+ {{time .CreatedAt}} |
+
+ {{else}}
+ Заданий пока нет. Нажмите «Запустить проверку», загрузите небольшой TSV и оставьте worker запущенным. |
+ {{end}}
+
+
+
+ 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"}}
+ ← К списку заданий
+ {{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-задачи
+ Если есть ошибки, в этой таблице видно код и сообщение. Подробные строки обновляются после обычного обновления страницы.
+ | Shard | Состояние | Попытка | Worker / lease | Ошибка |
{{range .Tasks}}| #{{.ChunkIndex}} | {{statusLabel .Status}} | {{.Attempt}} / {{.MaxAttempts}} | {{if .LeaseOwner}}{{.LeaseOwner}}{{if .LeaseExpiresAt}} до {{time .LeaseExpiresAt}}{{end}}{{else}}—{{end}} | {{if .ErrorCode}}{{.ErrorCode}} {{.ErrorMessage}}{{else}}—{{end}} |
{{else}}| Задачи пока не появились. |
{{end}}
+
+ Артефакты coordinator
+ | Тип | Файл | Размер | Проверка целостности | |
{{range .Artifacts}}{{if .Diagnostic}}Частичный результат диагностический{{else}}{{.Kind}}{{end}} | {{.Filename}} | {{bytes .SizeBytes}} | {{.SHA256}} | {{if .Downloadable}}Скачать CSV{{else}}Недоступно{{end}} |
{{else}}| Артефактов ещё нет: worker загрузит CSV после выполнения shard. |
{{end}}
+
+
+
+
+{{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"}}This creates a diagnostic shard job. Use query_smiles; global multi-shard reduction is not available yet.
+ ← К списку заданий
+ Пошаговый запуск
+ Поиск похожих молекул
+ Создаёт диагностическую задачу similarity-search: worker найдёт top-k молекул, похожих на заданный SMILES.
+
+
+ Перед запуском
+ - Оставьте хотя бы один
scimesh-worker запущенным. - Для ручной проверки используйте небольшой TSV.
- «Строк в shard» не ограничивает размер файла. Он делит весь загруженный файл на задачи: большой ChEMBL TSV при 1000 строках создаст тысячи задач.
+
+
+
+
+
+
+
+{{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)
+ }
+}