diff --git a/docs/reports/A48.md b/docs/reports/A48.md index 9a6abbb..1532621 100644 --- a/docs/reports/A48.md +++ b/docs/reports/A48.md @@ -1,4 +1,4 @@ -# A48 — промежуточный отчёт, НЕ ГОТОВО К ПРИЁМКЕ +# A48 — реализация и визуальная проверка Дата: 2026-08-31. Исполнитель: Codex, один исполнитель, без субагентов. @@ -7,7 +7,7 @@ - START_HEAD: `80aab00ee2bef353e12d128bd8ba6a6dde503abd`. - origin/main перед началом: `80aab00ee2bef353e12d128bd8ba6a6dde503abd`. - Ветка: `codex/a48-interface-by-mockup`, отдельный worktree `hermes-hub-a48`. -- FINAL_HEAD / FINAL_COMMIT_SHA: **ещё не определён — визуальная приёмка не завершена**. SHA промежуточного коммита передаётся в сообщении исполнителя. +- FINAL_COMMIT_SHA передаётся в итоговом сообщении после фиксации этого отчёта. Визуальное согласование владельцем не подменяется технической проверкой. - Главный checkout и незакоммиченная `.agents/` сохранены; ветка A47 не переключалась. - Живой хаб фактически слушает порт 5800, сообщает сборку `81a58f6`. На 8080 работает другое приложение; оно не изменялось. - Предпросмотр клиента A48: loopback 5801, защищённый API пересылается в существующий хаб без добавления авторизации и без изменения служб. Для другого порта браузеру требуется отдельный вход владельца. @@ -45,15 +45,19 @@ HubSnapshot: generation, seq, timestamp, profiles_by_provider, all_profiles, rea ## Визуальные доказательства и пропуски -[Галерея макет / до / промежуточная вёрстка](../screenshots/a48/index.html). +[Галерея макет / до / после](../screenshots/a48/index.html). - Собраны **21 исходный снимок**: 7 экранов × 3 темы, авторизованный живой хаб. - Собраны 21 снимок `layout-only` без входа для проверки базовой сетки/тем. Они НЕ заменяют требуемые скриншоты после с реальными данными; часть последних мелких CSS-правок требует повторной съёмки. -- **ПРОПУЩЕНО / ожидает входа:** 21 авторизованный снимок после, инспектор на живом снапшоте, панорамирование/зум/узел за край/вписать, читаемость подписей на нескольких масштабах, проверка действий маршрутизации и выбора модели с подключёнными аккаунтами, повторный полный визуальный проход. +- Собраны **21 авторизованный снимок после**, 1280×720. Дополнительно: Обзор 1536×1024, адаптивность 760×900, холст после переноса за край и Fit, нижние части аналитики/состояния/настроек. Проверены темы, переходы, поиск агента, вкладки инспектора и чтение Agent File. +- Панорамирование меняет translate; зум проверен 85→95%. После перетаскивания узла за край в EDIT и Fit: 13/13 узлов внутри холста, пересечений подписей с узлами нет. Черновик теста не сохранён на сервер. Ошибок консоли нет. +- **НЕ ПРОВЕРЕНО на живых данных:** заполненные карточки аккаунтов, перестановка аккаунтов в цепочке, выбор обнаруженной модели, детали реального события: API возвращает 0 подключённых аккаунтов и 0 событий workflow. Контракты списков моделей и отсутствующих значений проверены JS-тестами. Новые аккаунты и фиктивные события для скриншотов не создавались. - **ПРОПУЩЕНО намеренно:** кнопки несуществующих приложений экосистемы, выдуманные графики/рост процентов/кольцо 42, версия v2.9.0, неподдерживаемые настройки уведомлений/backup/2FA, история температур и сетевых замеров без источника, вымышленный профиль Administrator. -- Отличия от макета: используется существующий утверждённый растровый логотип с кремовой подложкой, а не генерация нового вензеля; нет фиктивных аккаунтов и назначений. Точные плотность и отступы окончательно не приняты. +- Отличия от макета: используется существующий утверждённый растровый логотип с кремовой подложкой, а не генерация нового вензеля; нет фиктивных аккаунтов и назначений. Сохранённые координаты графа не менялись: основная цепочка остаётся горизонтальной, остальные узлы разнесены. Поэтому геометрия графа отличается от демонстрационного дерева макета. При 1280×720 нижние панели требуют прокрутки. - Ограничение backend: `set_model` меняет предпочтение профиля, общее для его использования в разных ролях. Независимые модели на каждую пару роль/аккаунт нельзя честно обещать без отдельного изменения backend. Python в A48 не менялся. -## Продолжение +## Доработка после замечания владельца -После входа владельца на 5801: перезагрузить клиент, проверить консоль и DOM, исправить найденные дефекты, снять авторизованные после для всех тем, пройти холст, обновить галерею и отчёт. Только затем назначить FINAL_COMMIT_SHA и пометить A48 выполненным. +Переключатель LIVE/EDIT перенесён в шапку; KPI на desktop в одном ряду; инспектор занимает правый столбец рядом с графом и нижними карточками. Уплотнены шапка и холст. Подписи лежат на линиях связей, а не отдельно от них. Начальный вид сфокусирован на оркестраторе, Fit учитывает настоящие размеры узлов и подписей. Перетаскивание продолжает работать вне холста до отпускания мыши. Исправлены критический индикатор, объяснение сетевого замера, нулевая задержка в таблицах и отображение ранее настроенной, но неподтверждённой модели. + +Оригинальная пользовательская вкладка с несохранённым графом не перезагружалась. Проверки выполнялись в отдельной вкладке того же защищённого preview. Python, службы и установленный хаб не менялись. Нужна последующая функциональная проверка на подключённых аккаунтах; визуальную приёмку выполняет владелец. diff --git a/docs/screenshots/a48/after/accounts-dark.png b/docs/screenshots/a48/after/accounts-dark.png new file mode 100644 index 0000000..75f5a82 Binary files /dev/null and b/docs/screenshots/a48/after/accounts-dark.png differ diff --git a/docs/screenshots/a48/after/accounts-light.png b/docs/screenshots/a48/after/accounts-light.png new file mode 100644 index 0000000..2ea0b44 Binary files /dev/null and b/docs/screenshots/a48/after/accounts-light.png differ diff --git a/docs/screenshots/a48/after/accounts-medium.png b/docs/screenshots/a48/after/accounts-medium.png new file mode 100644 index 0000000..0769911 Binary files /dev/null and b/docs/screenshots/a48/after/accounts-medium.png differ diff --git a/docs/screenshots/a48/after/analytics-dark.png b/docs/screenshots/a48/after/analytics-dark.png new file mode 100644 index 0000000..99de352 Binary files /dev/null and b/docs/screenshots/a48/after/analytics-dark.png differ diff --git a/docs/screenshots/a48/after/analytics-light.png b/docs/screenshots/a48/after/analytics-light.png new file mode 100644 index 0000000..37b9c07 Binary files /dev/null and b/docs/screenshots/a48/after/analytics-light.png differ diff --git a/docs/screenshots/a48/after/analytics-lower-dark.png b/docs/screenshots/a48/after/analytics-lower-dark.png new file mode 100644 index 0000000..ff1a937 Binary files /dev/null and b/docs/screenshots/a48/after/analytics-lower-dark.png differ diff --git a/docs/screenshots/a48/after/analytics-medium.png b/docs/screenshots/a48/after/analytics-medium.png new file mode 100644 index 0000000..dde998b Binary files /dev/null and b/docs/screenshots/a48/after/analytics-medium.png differ diff --git a/docs/screenshots/a48/after/canvas-fit-after-drag.png b/docs/screenshots/a48/after/canvas-fit-after-drag.png new file mode 100644 index 0000000..51e7c48 Binary files /dev/null and b/docs/screenshots/a48/after/canvas-fit-after-drag.png differ diff --git a/docs/screenshots/a48/after/health-dark.png b/docs/screenshots/a48/after/health-dark.png new file mode 100644 index 0000000..e342500 Binary files /dev/null and b/docs/screenshots/a48/after/health-dark.png differ diff --git a/docs/screenshots/a48/after/health-light.png b/docs/screenshots/a48/after/health-light.png new file mode 100644 index 0000000..0388174 Binary files /dev/null and b/docs/screenshots/a48/after/health-light.png differ diff --git a/docs/screenshots/a48/after/health-lower-dark.png b/docs/screenshots/a48/after/health-lower-dark.png new file mode 100644 index 0000000..e02c94e Binary files /dev/null and b/docs/screenshots/a48/after/health-lower-dark.png differ diff --git a/docs/screenshots/a48/after/health-medium.png b/docs/screenshots/a48/after/health-medium.png new file mode 100644 index 0000000..b146582 Binary files /dev/null and b/docs/screenshots/a48/after/health-medium.png differ diff --git a/docs/screenshots/a48/after/logs-dark.png b/docs/screenshots/a48/after/logs-dark.png new file mode 100644 index 0000000..2e86954 Binary files /dev/null and b/docs/screenshots/a48/after/logs-dark.png differ diff --git a/docs/screenshots/a48/after/logs-light.png b/docs/screenshots/a48/after/logs-light.png new file mode 100644 index 0000000..3e2c71b Binary files /dev/null and b/docs/screenshots/a48/after/logs-light.png differ diff --git a/docs/screenshots/a48/after/logs-medium.png b/docs/screenshots/a48/after/logs-medium.png new file mode 100644 index 0000000..5353f48 Binary files /dev/null and b/docs/screenshots/a48/after/logs-medium.png differ diff --git a/docs/screenshots/a48/after/overview-760.png b/docs/screenshots/a48/after/overview-760.png new file mode 100644 index 0000000..67b53e3 Binary files /dev/null and b/docs/screenshots/a48/after/overview-760.png differ diff --git a/docs/screenshots/a48/after/overview-dark.png b/docs/screenshots/a48/after/overview-dark.png new file mode 100644 index 0000000..96eafbb Binary files /dev/null and b/docs/screenshots/a48/after/overview-dark.png differ diff --git a/docs/screenshots/a48/after/overview-light.png b/docs/screenshots/a48/after/overview-light.png new file mode 100644 index 0000000..d2abcb0 Binary files /dev/null and b/docs/screenshots/a48/after/overview-light.png differ diff --git a/docs/screenshots/a48/after/overview-medium.png b/docs/screenshots/a48/after/overview-medium.png new file mode 100644 index 0000000..2aee524 Binary files /dev/null and b/docs/screenshots/a48/after/overview-medium.png differ diff --git a/docs/screenshots/a48/after/overview-reference-size.png b/docs/screenshots/a48/after/overview-reference-size.png new file mode 100644 index 0000000..af33ed0 Binary files /dev/null and b/docs/screenshots/a48/after/overview-reference-size.png differ diff --git a/docs/screenshots/a48/after/routing-dark.png b/docs/screenshots/a48/after/routing-dark.png new file mode 100644 index 0000000..d9e7ba4 Binary files /dev/null and b/docs/screenshots/a48/after/routing-dark.png differ diff --git a/docs/screenshots/a48/after/routing-light.png b/docs/screenshots/a48/after/routing-light.png new file mode 100644 index 0000000..a9bdf31 Binary files /dev/null and b/docs/screenshots/a48/after/routing-light.png differ diff --git a/docs/screenshots/a48/after/routing-medium.png b/docs/screenshots/a48/after/routing-medium.png new file mode 100644 index 0000000..d046539 Binary files /dev/null and b/docs/screenshots/a48/after/routing-medium.png differ diff --git a/docs/screenshots/a48/after/settings-dark.png b/docs/screenshots/a48/after/settings-dark.png new file mode 100644 index 0000000..9fed625 Binary files /dev/null and b/docs/screenshots/a48/after/settings-dark.png differ diff --git a/docs/screenshots/a48/after/settings-light.png b/docs/screenshots/a48/after/settings-light.png new file mode 100644 index 0000000..6bb613b Binary files /dev/null and b/docs/screenshots/a48/after/settings-light.png differ diff --git a/docs/screenshots/a48/after/settings-lower-dark.png b/docs/screenshots/a48/after/settings-lower-dark.png new file mode 100644 index 0000000..0f32133 Binary files /dev/null and b/docs/screenshots/a48/after/settings-lower-dark.png differ diff --git a/docs/screenshots/a48/after/settings-medium.png b/docs/screenshots/a48/after/settings-medium.png new file mode 100644 index 0000000..75a47e5 Binary files /dev/null and b/docs/screenshots/a48/after/settings-medium.png differ diff --git a/docs/screenshots/a48/index.html b/docs/screenshots/a48/index.html index 9dfb91c..00dcb63 100644 --- a/docs/screenshots/a48/index.html +++ b/docs/screenshots/a48/index.html @@ -1,4 +1,4 @@ -A48 — визуальная проверка, НЕ ПРИНЯТО +A48 — визуальное сравнение -

A48 · Сравнение с макетами

Промежуточный материал. A48 не принято. «До»: живой хаб, сборка 81a58f6, 21 снимок. Справа: только проверка вёрстки без авторизованных данных, НЕ приёмочные скриншоты «после». Полный проход инспектора, холста и тем после входа ещё не выполнен.

База кода: 80aab00. Контрольная ширина снимков: 1280 px. Макеты имеют разные исходные размеры; изображения показаны полностью, без подмены данных.

- +

A48 · Сравнение с макетами

Клиент проверен с живыми данными; визуальное согласование остаётся за владельцем. «До»: хаб 81a58f6. «После»: клиент A48 с тем же защищённым API. 21 снимок до и 21 после. Аккаунтов 0, поэтому заполненные карточки и выбор реальной модели не проверены.

База кода: 80aab00. Контрольная ширина снимков: 1280 px. Макеты имеют разные исходные размеры; изображения показаны полностью, без подмены данных.

Обзор 1536×1024 · Граф после перетаскивания и вписывания · Адаптивность 760 px

+ diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index 96189f5..ae65cdf 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -1268,7 +1268,7 @@ function renderAnalyticsView() { ${item.total_calls ?? 0} ${item.successful_calls ?? 0} ${item.failed_calls ?? 0} - ${item.latency_p50_ms ? `${item.latency_p50_ms} мс` : '—'} + ${Number.isFinite(item.latency_p50_ms) ? `${item.latency_p50_ms} мс` : '—'} ${item.total_tokens != null ? new Intl.NumberFormat('ru-RU').format(item.total_tokens) : 'Н/Д'} `; @@ -1307,7 +1307,7 @@ function renderAnalyticsView() { ${item.total_calls ?? 0} ${item.successful_calls ?? 0} ${item.failed_calls ?? 0} - ${item.latency_p50_ms ? `${item.latency_p50_ms} мс` : '—'} + ${Number.isFinite(item.latency_p50_ms) ? `${item.latency_p50_ms} мс` : '—'} `; }); diff --git a/src/antigravity_provider/router/web/static/index.html b/src/antigravity_provider/router/web/static/index.html index e534c8f..0f44855 100644 --- a/src/antigravity_provider/router/web/static/index.html +++ b/src/antigravity_provider/router/web/static/index.html @@ -11,7 +11,7 @@ - +
+
+ + +
H
Локальный хабПрофиль пользователя: Н/Д
- -
+
Сохранено @@ -142,12 +143,12 @@ -
Последние события

Загрузка событий…

Статистика workflow
Н/Д: снапшот ещё не получен
Активные задачи · LIVE

+ diff --git a/src/antigravity_provider/router/web/static/style.css b/src/antigravity_provider/router/web/static/style.css index 992aa8e..b677047 100644 --- a/src/antigravity_provider/router/web/static/style.css +++ b/src/antigravity_provider/router/web/static/style.css @@ -1904,3 +1904,14 @@ body[data-theme="medium"] .nav-icon { stroke:var(--canvas-ink); } body[data-theme="medium"] .brand-title { color:var(--canvas-ink); } body[data-theme="medium"] .nav-item.active .nav-icon { stroke:var(--text-accent); } .header-icon svg { width:22px; height:22px; fill:none; stroke:currentColor; stroke-width:1.5; } +.header-update-badge { color:var(--chrome-ink); background:transparent; } +@media(min-width:1051px) { + .top-header { min-height:84px; padding-top:8px; padding-bottom:8px; } + .header-left { display:flex; flex-wrap:wrap; gap:6px; min-width:200px; } + .header-title { width:100%; } + .header-readiness-badge,.header-update-badge { margin:0; font-size:9px; padding:4px 6px; } +} +.readiness-banner.not-ready { border-left-color:var(--status-error); } +.status-dot.not-ready { background:var(--status-error); } +.status-dot.ready { background:var(--status-healthy); } +.resource-value { font-size:clamp(18px,1.7vw,26px); } diff --git a/src/antigravity_provider/router/web/static/workflow.css b/src/antigravity_provider/router/web/static/workflow.css index 5d28bcf..a512d66 100644 --- a/src/antigravity_provider/router/web/static/workflow.css +++ b/src/antigravity_provider/router/web/static/workflow.css @@ -148,3 +148,43 @@ dt { color:var(--text-muted); } dd { text-align:right; color:var(--text-secondar @media(max-width:1250px) { .workflow-kpis { grid-template-columns:repeat(3,minmax(0,1fr)); }.workflow-main-layout { grid-template-columns:minmax(0,1fr) 300px; } } @media(max-width:950px) { .workflow-main-layout { grid-template-columns:1fr; }.workflow-inspector { max-height:none; }.workflow-bottom-grid { grid-template-columns:1fr; } } @media(max-width:650px) { .workflow-kpis { grid-template-columns:repeat(2,minmax(0,1fr)); }.workflow-toolbar { flex-wrap:wrap; }.workflow-toolbar-actions { flex-wrap:wrap; }.workflow-canvas { min-height:400px; } } + +/* Match the reference: one KPI row; inspector spans graph and lower cards. */ +body[data-view="overview"] .header-page-actions { display:none; } +body:not([data-view="overview"]) .top-header>.workflow-mode { display:none; } +.workflow-toolbar { display:none; } +body[data-workflow-mode="edit"] .workflow-toolbar { display:flex; justify-content:flex-end; } +.workflow-kpis { grid-template-columns:repeat(6,minmax(0,1fr)); } +.workflow-kpi { min-height:86px; padding:11px 10px; } +.workflow-kpi strong { font-size:18px; } +.workflow-main-layout { grid-template-columns:minmax(0,1fr) 30%; grid-template-rows:auto auto; min-height:0; gap:10px; } +.workflow-board-card { grid-column:1; grid-row:1; } +.workflow-inspector { grid-column:2; grid-row:1 / span 2; max-height:calc(100vh - 204px); } +.workflow-bottom-grid { grid-column:1; grid-row:2; margin:0; min-width:0; align-items:stretch; } +.workflow-bottom-grid>section { min-width:0; min-height:170px; } +.workflow-canvas { min-height:300px; height:clamp(300px,calc(100vh - 460px),520px); flex:auto; } +.workflow-board-header { min-height:44px; padding:8px 10px; } +.workflow-board-header>div:first-child { flex-basis:100%; } +.workflow-board-header strong { text-transform:none; } +.workflow-legend { gap:8px; padding:6px 10px; font-size:9px; } +.workflow-run-panel textarea { min-height:46px; } +.workflow-run-panel .btn { padding:6px; font-size:10px; } +.workflow-events header { flex-wrap:wrap; gap:6px; } +.workflow-statistics dl { grid-template-columns:1fr; gap:4px; } +.workflow-statistics dd { text-align:left; margin-bottom:6px; } +.workflow-statistics p { font-size:10px; } +.workflow-events-list { max-height:150px; } +@media(min-width:1450px) { .workflow-board-header>div:first-child { flex-basis:220px; } } +@media(max-width:1050px) { .workflow-kpis { grid-template-columns:repeat(3,minmax(0,1fr)); } .workflow-bottom-grid { grid-template-columns:1fr; } } +@media(max-width:950px) { .workflow-main-layout { grid-template-columns:1fr; } .workflow-inspector { grid-column:1; grid-row:2; max-height:580px; } .workflow-bottom-grid { grid-row:3; grid-template-columns:repeat(3,minmax(0,1fr)); } } +@media(max-width:650px) { .workflow-kpis { grid-template-columns:repeat(2,minmax(0,1fr)); } .workflow-bottom-grid { grid-template-columns:1fr; } } +@media(min-width:1051px) { + body[data-view="overview"] .top-header { min-height:74px; padding-top:8px; padding-bottom:8px; } + body[data-view="overview"] .header-left { display:grid; grid-template-columns:max-content max-content; gap:6px 10px; } + body[data-view="overview"] .header-title { grid-column:1 / -1; } + body[data-view="overview"] .header-readiness-badge,body[data-view="overview"] .header-update-badge { margin:0; font-size:9px; padding:4px 6px; } +} +.workflow-canvas { height:clamp(300px,calc(100vh - 570px),430px); } +.workflow-inspector .inspector-section { padding:10px; margin:8px 0; } +.workflow-inspector .inspector-section p { line-height:1.4; margin:6px 0; } +.workflow-inspector dl { gap:6px 10px; margin:8px 0; } diff --git a/src/antigravity_provider/router/web/static/workflow.js b/src/antigravity_provider/router/web/static/workflow.js index 194f55c..770d4ec 100644 --- a/src/antigravity_provider/router/web/static/workflow.js +++ b/src/antigravity_provider/router/web/static/workflow.js @@ -84,9 +84,9 @@ function initWorkflowOverview() { canvas.style.cursor = 'grabbing'; } }); - canvas?.addEventListener('mousemove', workflowPointerMove); - canvas?.addEventListener('mouseup', workflowPointerUp); - canvas?.addEventListener('mouseleave', workflowPointerCancel); + window.addEventListener('mousemove', workflowPointerMove); + window.addEventListener('mouseup', workflowPointerUp); + window.addEventListener('blur', workflowPointerCancel); window.addEventListener('resize', () => { drawWorkflowEdges(); }); @@ -120,7 +120,7 @@ function renderWorkflowOverview(snapshot) { renderWorkflowInspector(snapshot, workflow); renderWorkflowEvents(workflow.events || []); renderWorkflowStatistics(workflow); - if (!workflowUi.fitted && workflow.agents?.length) { workflowUi.fitted = true; requestAnimationFrame(fitWorkflowGraph); } + if (!workflowUi.fitted && workflow.agents?.length) { workflowUi.fitted = true; requestAnimationFrame(focusWorkflowStart); } requestAnimationFrame(drawWorkflowEdges); } @@ -236,6 +236,7 @@ function setWorkflowMode(mode) { return; } workflowUi.mode = mode; + document.body.dataset.workflowMode = mode; document.querySelectorAll('.workflow-mode-btn').forEach((button) => button.classList.toggle('active', button.dataset.mode === mode)); renderWorkflowOverview(currentSnapshot); } @@ -268,7 +269,7 @@ function workflowPointerMove(event) { const dx = (event.clientX - drag.startX) / workflowUi.scale; const dy = (event.clientY - drag.startY) / workflowUi.scale; if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true; - workflowUi.draftPositions[drag.id] = { x: Math.max(0, drag.original.x + dx), y: Math.max(0, drag.original.y + dy) }; + workflowUi.draftPositions[drag.id] = { x: drag.original.x + dx, y: drag.original.y + dy }; const node = document.querySelector(`.workflow-node[data-agent-id="${CSS.escape(drag.id)}"]`); if (node) { node.style.left = `${workflowUi.draftPositions[drag.id].x}px`; @@ -340,18 +341,17 @@ function drawWorkflowEdges() { const y1 = source.offsetTop + source.offsetHeight / 2; const x2 = target.offsetLeft; const y2 = target.offsetTop + target.offsetHeight / 2; - const bend = Math.max(45, Math.abs(x2 - x1) * 0.42); - const path = `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}`; const klass = String(edge.condition || '').toLowerCase(); const label = edge.label || edge.condition; const labelWidth = Math.max(70, String(label).length * 6 + 16); const labelX = (x1 + x2) / 2; - let labelY = (y1 + y2) / 2 - 12; + let labelY = Math.min(source.offsetTop, target.offsetTop) - 30; const boxes = [...document.querySelectorAll('.workflow-node')].map(node => ({left:node.offsetLeft,top:node.offsetTop,width:node.offsetWidth,height:node.offsetHeight})); - const overlaps = box => labelX + labelWidth/2 > box.left - 6 && labelX - labelWidth/2 < box.left + box.width + 6 && labelY + 10 > box.top - 6 && labelY - 12 < box.top + box.height + 6; - while ([...boxes,...occupiedLabels].some(overlaps)) labelY += 24; - occupiedLabels.push({left:labelX-labelWidth/2,top:labelY-12,width:labelWidth,height:22}); - labels.push(`${wfEscape(label)}`); + const overlaps = box => labelX + labelWidth/2 > box.left - 6 && labelX - labelWidth/2 < box.left + box.width + 6 && labelY + 12 > box.top - 6 && labelY - 12 < box.top + box.height + 6; + while ([...boxes,...occupiedLabels].some(overlaps)) labelY -= 30; + occupiedLabels.push({left:labelX-labelWidth/2,top:labelY-12,width:labelWidth,height:24}); + const path = `M ${x1} ${y1} C ${x1+28} ${y1}, ${x1+28} ${labelY}, ${x1} ${labelY} L ${x2} ${labelY} C ${x2-28} ${labelY}, ${x2-28} ${y2}, ${x2} ${y2}`; + labels.push(`${wfEscape(label)}`); parts.push(``); }); layer.innerHTML = parts.join(''); @@ -377,7 +377,7 @@ function setWorkflowScale(value, focusX, focusY) { const canvas = document.getElementById('workflow-canvas'); if (!canvas) return; const oldScale = workflowUi.scale; - const newScale = Math.max(0.3, Math.min(2.5, Math.round(value * 100) / 100)); + const newScale = Math.max(0.1, Math.min(2.5, Math.round(value * 100) / 100)); if (focusX === undefined || focusY === undefined) { focusX = canvas.clientWidth / 2; focusY = canvas.clientHeight / 2; @@ -388,6 +388,16 @@ function setWorkflowScale(value, focusX, focusY) { updateCanvasTransform(); } +function focusWorkflowStart() { + const canvas = document.getElementById('workflow-canvas'); + const node = document.querySelector('.workflow-node.selected') || document.querySelector('.workflow-node'); + if (!canvas || !node) return; + workflowUi.scale = .85; + workflowUi.panX = 80 - node.offsetLeft * workflowUi.scale; + workflowUi.panY = 110 - node.offsetTop * workflowUi.scale; + updateCanvasTransform(); +} + function fitWorkflowGraph() { const canvas = document.getElementById('workflow-canvas'); if (!canvas) return; @@ -403,9 +413,16 @@ function fitWorkflowGraph() { agents.forEach((agent) => { const pos = workflowUi.draftPositions[agent.id] || agent.position || { x: 80, y: 80 }; minX = Math.min(minX, pos.x); - maxX = Math.max(maxX, pos.x + 250); + const node = document.querySelector(`.workflow-node[data-agent-id="${CSS.escape(agent.id)}"]`); + maxX = Math.max(maxX, pos.x + (node?.offsetWidth || 250)); minY = Math.min(minY, pos.y); - maxY = Math.max(maxY, pos.y + 124); + maxY = Math.max(maxY, pos.y + (node?.offsetHeight || 124)); + }); + document.querySelectorAll('.workflow-label-bg').forEach(label => { + const x = Number(label.getAttribute('x')), y = Number(label.getAttribute('y')); + minX = Math.min(minX, x); minY = Math.min(minY, y); + maxX = Math.max(maxX, x + Number(label.getAttribute('width'))); + maxY = Math.max(maxY, y + Number(label.getAttribute('height'))); }); const width = maxX - minX || 200; const height = maxY - minY || 120; @@ -501,6 +518,7 @@ function renderAgentModelTab(content, snapshot, agent) { const providerSelect = document.getElementById('agent-provider'); const accountSelect = document.getElementById('agent-account'); const modelSelect = document.getElementById('agent-model'); + let initialRendering = true; const refreshAccounts = () => { const matches = profiles.filter((profile) => profile.provider === providerSelect.value); accountSelect.innerHTML = matches.length ? matches.map((profile) => ``).join('') : ''; @@ -508,12 +526,13 @@ function renderAgentModelTab(content, snapshot, agent) { }; const refreshModels = () => { const profile = profiles.find((item) => item.profile_id === accountSelect.value); - const selected = profile?.profile_id === cfg.account && providerSelect.value === cfg.provider ? cfg.model : ''; + const selected = initialRendering || (profile?.profile_id === cfg.account && providerSelect.value === cfg.provider) ? cfg.model : ''; modelSelect.innerHTML = modelOptions(snapshot, profile || { provider: providerSelect.value }, selected); }; providerSelect.onchange = refreshAccounts; accountSelect.onchange = refreshModels; refreshAccounts(); + initialRendering = false; document.getElementById('agent-model-save').onclick = () => executeAction('update_agent', { agent_id: agent.id, provider: providerSelect.value, profile_id: accountSelect.value, model: modelSelect.value, temperature: nullableNumber('agent-temperature'), max_tokens: nullableNumber('agent-max-tokens'), timeout: nullableNumber('agent-timeout'), diff --git a/src/antigravity_provider/router/web/static/workspace.js b/src/antigravity_provider/router/web/static/workspace.js index f6fecac..edb881e 100644 --- a/src/antigravity_provider/router/web/static/workspace.js +++ b/src/antigravity_provider/router/web/static/workspace.js @@ -85,7 +85,7 @@ function renderHostResources(container, host) { ['Память', measured(host.memory_percent,'%'), Number.isFinite(host.memory_used_mb) ? `${Math.round(host.memory_used_mb)} / ${Math.round(host.memory_total_mb)} МБ` : 'Н/Д: RAM не измерена'], ['GPU', 'Н/Д', 'API не передаёт GPU'], ['Диск', measured(host.disk_percent,'%'), Number.isFinite(host.disk_used_gb) ? `${host.disk_used_gb} / ${host.disk_total_gb} ГБ` : 'Н/Д: диск не измерен'], - ['Сеть', measured(host.net_speed_mbps,' Мбит/с'), 'Н/Д до второго замера счётчиков'], + ['Сеть', measured(host.net_speed_mbps,' Мбит/с'), Number.isFinite(host.net_speed_mbps) ? 'Скорость между замерами счётчиков' : 'Н/Д: требуется второй замер'], ]; container.innerHTML = items.map(([label,value,reason]) => `
${label}
${value}
${value === 'Н/Д' ? `Н/Д: ${label} не получен от API` : reason}
`).join(''); }