hermes-hub/src/antigravity_provider/router/web/static/index.html

852 lines
60 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hermes Hub — Панель управления</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600;700;800&family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="static/workflow.css">
<link rel="icon" href="static/hermes-logo.png">
</head>
<body data-view="overview" data-workflow-mode="live">
<div id="app" class="app-layout">
<!-- Seven workspaces share the same navigation. -->
<aside class="sidebar">
<div class="brand">
<img class="brand-logo" src="static/hermes-logo.png" alt="Логотип Hermes Hub">
<div class="brand-text">
<span class="brand-title">HERMES HUB</span>
<span class="brand-subtitle">Крона · Бизнес-экосистема</span>
</div>
</div>
<nav class="nav-menu">
<button class="nav-item active" data-view="overview">
<svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 10 12 3l9 7v11h-6v-7H9v7H3Z"/></svg>
<span class="nav-label">Обзор</span>
</button>
<button class="nav-item" data-view="accounts">
<svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M16 21v-3a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v3 M9 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8 M17 3a4 4 0 0 1 0 8 M22 21v-3a4 4 0 0 0-3-4"/></svg>
<span class="nav-label">Аккаунты</span>
<span class="nav-badge" id="nav-accounts-count"></span>
</button>
<button class="nav-item" data-view="routing">
<svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4v16 M5 8h14 M5 16h14 M16 5l3 3-3 3 M16 13l3 3-3 3"/></svg>
<span class="nav-label">Маршрутизация</span>
</button>
<button class="nav-item" data-view="skills">
<svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
<span class="nav-label">Скиллы</span>
<span class="nav-badge" id="nav-skills-count"></span>
</button>
<button class="nav-item" data-view="analytics">
<svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 3v18h18 M7 16v-5 M12 16V7 M17 16v-8"/></svg>
<span class="nav-label">Аналитика</span>
</button>
<button class="nav-item" data-view="health">
<svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2 3 6v6c0 5 9 10 9 10s9-5 9-10V6Z M8 12l3 3 5-6"/></svg>
<span class="nav-label">Состояние системы</span>
</button>
<button class="nav-item" data-view="logs">
<svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 2h9l4 4v16H5V2Z M8 10h8 M8 14h8 M8 18h5"/></svg>
<span class="nav-label">Журнал событий</span>
</button>
<button class="nav-item" data-view="settings">
<svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3h6l1 3 3 1 2 5-2 5-3 1-1 3H9l-1-3-3-1-2-5 2-5 3-1Z M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8"/></svg>
<span class="nav-label">Настройки</span>
</button>
</nav>
<div class="brand-panel"><img src="static/hermes-logo.png" alt=""><p>Рабочее пространство<br>вашей агентной системы</p><span>Hermes Hub · Крона</span></div>
<div class="sidebar-footer">
<div class="source-indicator" id="source-indicator">
<span class="status-dot healthy"></span>
<span class="source-text" id="source-text">Загрузка данных...</span>
</div>
<div class="version-tag" id="version-tag">Hermes Hub Web</div>
<div class="commit-tag" id="commit-tag" style="font-size:11px; color:var(--text-muted); font-family:var(--font-mono); margin-top:2px;"></div>
</div>
</aside>
<!-- Main Content Container -->
<main class="main-wrapper">
<!-- Top Header -->
<header class="top-header">
<div class="header-left">
<h1 class="header-title" id="page-title">Обзор системы</h1>
<span class="header-readiness-badge" id="header-readiness-badge">
<span class="status-dot"></span>
<span id="header-readiness-text">Инициализация...</span>
</span>
<span class="header-hermes-config-badge hidden" id="header-hermes-config-badge" style="display:inline-flex; align-items:center; font-size:12px; color:var(--text-muted); background:var(--surface-muted); padding:3px 8px; border-radius:var(--radius-sm); border:1px solid var(--border-subtle); margin-left:6px;"></span>
<button class="header-update-badge hidden" id="header-update-badge" title="Нажмите, чтобы посмотреть детали обновления">
<span class="status-dot warning"></span>
<span id="header-update-text">Доступно обновление</span>
</button>
</div>
<div class="workflow-mode" role="group" aria-label="Режим workflow">
<button class="workflow-mode-btn active" id="workflow-mode-live" data-mode="live">LIVE</button>
<button class="workflow-mode-btn" id="workflow-mode-edit" data-mode="edit">EDIT</button>
</div>
<div class="header-actions"><button class="header-search" id="hub-search-open" aria-label="Поиск по хабу"><span>⌕ Поиск</span><kbd>Ctrl K</kbd></button><button class="header-icon" id="hub-notifications" aria-label="Уведомления" title="Последние события"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0v6l2 3H4l2-3Z M9 20h6"/></svg></button><button class="header-icon" id="hub-settings" aria-label="Открыть настройки" title="Настройки"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3h6l1 3 3 1 2 5-2 5-3 1-1 3H9l-1-3-3-1-2-5 2-5 3-1Z M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8"/></svg></button><div class="hub-user" title="Н/Д: API не передаёт профиль пользователя"><span>H</span><div>Локальный хаб<small>Профиль пользователя: Н</small></div></div></div><div class="header-page-actions">
<button class="btn btn-secondary" id="btn-refresh-all" title="Обновить квоты всех профилей">
<span class="btn-icon"></span>
<span>Обновить всё</span>
</button>
<button class="btn btn-primary" id="btn-add-account">
<span class="btn-icon">+</span>
<span>Добавить аккаунт</span>
</button>
</div>
</header>
<!-- Dynamic Views (7 Control Sections) -->
<div class="content-scroll">
<!-- 1. OVERVIEW VIEW -->
<section id="view-overview" class="view-pane active">
<div class="workflow-toolbar">
<div class="workflow-toolbar-actions">
<span class="workflow-save-state" id="workflow-save-state">Сохранено</span>
<button class="btn btn-secondary" id="btn-workflow-save">Сохранить workflow</button>
<button class="btn btn-primary" id="btn-agent-add">+ Добавить агента</button>
</div>
</div>
<div class="workflow-kpis" aria-label="Оперативные показатели">
<article class="workflow-kpi"><span>Активные задачи</span><strong id="workflow-kpi-active">Загрузка…</strong><small id="workflow-kpi-active-reason">Получение снапшота</small></article>
<article class="workflow-kpi"><span>Готовность ролей</span><strong id="workflow-kpi-online">Загрузка…</strong><small id="workflow-kpi-online-reason">Получение снапшота</small></article>
<article class="workflow-kpi"><span>Ответ модели · p50</span><strong id="workflow-kpi-latency">Загрузка…</strong><small id="workflow-kpi-latency-reason">Получение телеметрии</small></article>
<article class="workflow-kpi"><span>Использование токенов</span><strong id="workflow-kpi-tokens">Загрузка…</strong><small id="workflow-kpi-tokens-reason">Получение телеметрии</small></article>
<article class="workflow-kpi"><span>Успешность вызовов</span><strong id="workflow-kpi-success">Загрузка…</strong><small id="workflow-kpi-success-reason">Получение телеметрии</small></article>
<article class="workflow-kpi"><span>Статус системы</span><strong id="workflow-kpi-status">Загрузка…</strong><small id="workflow-kpi-status-reason">Проверка готовности</small></article>
</div>
<div class="workflow-main-layout">
<section class="workflow-board-card">
<header class="workflow-board-header">
<div><strong id="workflow-title">Workflow</strong> <span id="workflow-run-state" class="workflow-run-state">Загрузка…</span></div>
<label>Итерация: <strong id="workflow-iteration">Н</strong> / <input id="workflow-max-iterations" type="number" min="1" max="100" value="5"></label>
<div class="workflow-zoom"><button id="workflow-zoom-out" title="Уменьшить"></button><span id="workflow-zoom-value">100%</span><button id="workflow-zoom-in" title="Увеличить">+</button><button id="workflow-fit" title="Вписать граф"></button></div>
</header>
<div class="workflow-canvas" id="workflow-canvas" tabindex="0" aria-label="Редактор графа workflow">
<div class="canvas-tools" role="toolbar" aria-label="Инструменты холста"><button id="canvas-select" title="Выбор агента" aria-label="Выбор агента"></button><button id="canvas-add" title="Добавить агента" aria-label="Добавить агента на холст">+</button><button id="canvas-connect" title="Соединить агентов в EDIT" aria-label="Создать связь агентов"></button><button id="canvas-fit" title="Вписать граф" aria-label="Вписать весь граф"></button><button id="canvas-arrange" title="Разнести узлы (черновик)" aria-label="Разнести узлы"></button></div><div id="workflow-viewport" class="workflow-viewport">
<svg id="workflow-edges" class="workflow-edges" aria-hidden="true"><defs><marker id="wf-arrow-success" class="edge-success" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto" markerUnits="userSpaceOnUse"><path d="M1,1 L8,4.5 L1,8 L3,4.5 Z"/></marker><marker id="wf-arrow-return" class="edge-return" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto" markerUnits="userSpaceOnUse"><path d="M1,1 L8,4.5 L1,8 L3,4.5 Z"/></marker><marker id="wf-arrow-next" class="edge-next" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto" markerUnits="userSpaceOnUse"><path d="M1,1 L8,4.5 L1,8 L3,4.5 Z"/></marker></defs><g id="workflow-edge-layer"></g></svg>
<div id="workflow-node-layer" class="workflow-node-layer"></div><svg class="workflow-labels" aria-hidden="true"><g id="workflow-label-layer"></g></svg>
</div>
<div id="workflow-empty" class="workflow-empty hidden"><strong>В workflow пока нет агентов</strong><span>Добавьте агента, затем переключитесь в EDIT и соедините узлы.</span></div>
<div class="workflow-minimap" id="workflow-minimap" aria-label="Мини-карта"></div>
</div>
<footer class="workflow-legend">
<span><i class="state-waiting"></i>Ожидает</span><span><i class="state-working"></i>Работает</span><span><i class="state-reviewing"></i>Проверяет</span><span><i class="state-error"></i>Ошибка</span><span><i class="state-completed"></i>Завершено</span>
<span class="edge-legend success">→ Успех</span><span class="edge-legend return">⇢ Возврат / ошибка</span><span class="edge-legend next">→ Следующий шаг</span>
</footer>
</section>
<aside class="workflow-inspector" id="workflow-inspector">
<div class="workflow-inspector-empty"><strong>INSPECTOR</strong><span>Выберите агента на графе</span></div>
</aside>
<div class="workflow-bottom-grid">
<section class="workflow-events"><header><strong>Последние события</strong><button id="workflow-events-all">Журнал событий →</button></header><div id="workflow-events-list" class="workflow-events-list"><p>Загрузка событий…</p></div></section>
<section class="workflow-statistics"><header><strong>Статистика workflow</strong></header><div id="workflow-statistics-content">Н/Д: снапшот ещё не получен</div></section><section class="workflow-run-panel"><header><strong>Активные задачи · LIVE</strong></header><div id="workflow-active-task" class="inspector-value"></div><textarea aria-label="Задача workflow" id="workflow-task" placeholder="Опишите реальную задачу для workflow"></textarea><div><button class="btn btn-primary" id="btn-workflow-start">Запустить workflow</button><button class="btn btn-secondary" id="btn-workflow-stop">Остановить</button></div><p id="workflow-run-error" class="workflow-run-error"></p></section>
</div>
</div>
</section>
<!-- 2. ACCOUNTS VIEW -->
<section id="view-accounts" class="view-pane">
<div class="toolbar">
<div class="search-box">
<span class="search-icon">🔍</span>
<input type="text" id="accounts-search" placeholder="Поиск по аккаунту, почте, роли или модели...">
</div>
<div class="filters-row">
<select id="filter-provider" class="select-filter">
<option value="all">Все провайдеры</option>
<option value="antigravity">Google Antigravity</option>
<option value="openai-codex">OpenAI Codex</option>
<option value="opencode-go">OpenCode Go</option>
<option value="claude">Claude (Anthropic)</option>
<option value="grok">Grok (xAI)</option>
<option value="ollama">Ollama</option>
<option value="local">Локальные (Local LLM)</option>
<option value="openrouter">OpenRouter</option>
<option value="nvidia">NVIDIA NIM</option>
</select>
<select id="filter-health" class="select-filter">
<option value="all">Все состояния</option>
<option value="healthy">Работает</option>
<option value="warning">Предупреждение</option>
<option value="quota_exhausted">Квота исчерпана</option>
<option value="auth_required">Требуется вход</option>
<option value="disabled">Отключён / Резерв</option>
</select>
<button class="btn btn-secondary btn-sm" id="btn-export-quotas" onclick="openExportQuotasModal()" title="Выгрузить отчет о лимитах и квотах в формате JSON или CSV">
<span>📥 Экспорт лимитов (JSON / CSV)</span>
</button>
<div class="toolbar-stats" id="accounts-stats-summary">
Список аккаунтов ещё не получен
</div>
</div>
</div>
<div class="account-summary" id="account-summary"></div><div id="accounts-container" class="accounts-groups-container">
<!-- Rendered by app.js -->
</div>
</section>
<!-- 3. ROUTING VIEW (Main Control Center) -->
<section id="view-routing" class="view-pane">
<div class="routing-toolbar-actions">
<div style="display:flex; align-items:center; gap:8px;">
<label for="routing-default-role-select" style="font-weight:600; font-size:12px;">Роль по умолчанию:</label>
<select id="routing-default-role-select" class="select-filter" style="font-size:12px;">
<option value="manager">Главный менеджер (manager)</option>
<option value="developer-1">Кодер 1 (developer-1)</option>
<option value="developer-2">Кодер 2 (developer-2)</option>
<option value="code-reviewer">Ревьюер кода (code-reviewer)</option>
<option value="researcher">Исследователь (researcher)</option>
<option value="tester">Тестировщик (tester)</option>
<option value="integration-expert">Интегратор (integration-expert)</option>
<option value="security-expert">Эксперт по безопасности (security-expert)</option>
<option value="tech-writer">Технический писатель (tech-writer)</option>
<option value="analyst">Аналитик требований (analyst)</option>
<option value="dependency-agent">Агент зависимостей (dependency-agent)</option>
</select>
</div>
<button class="btn btn-secondary btn-sm" id="btn-routing-auto-assign" onclick="openAutoAssignPreviewModal()" title="Автоматическое распределение подключенных аккаунтов по ролям">
<span>⚡ Авто-распределение</span>
</button>
</div>
<div class="view-header-note" style="display:flex; justify-content: space-between; align-items: center;">
<div>
<i class="fa-solid fa-circle-info" style="color:var(--status-warning);"></i>
Перетащите аккаунт из правой панели в нужную роль и установите порядок (приоритет) использования.
<br>
Система будет использовать аккаунты сверху вниз по списку при выполнении задач с учётом квот и состояния.
</div>
</div>
<div class="routing-grid" style="display: flex; gap: 24px; height: calc(100vh - 150px);">
<!-- LEFT COLUMN: Roles -->
<div class="routing-left-col" id="routing-roles-container" style="flex: 2; overflow-y: auto; padding-right: 8px;">
<!-- Rendered by app.js -->
</div>
<!-- RIGHT COLUMN: Available Accounts -->
<div class="routing-right-col" style="flex: 1; display: flex; flex-direction: column; border: 1px solid var(--border); border-radius: var(--radius-md); background-color: var(--surface-muted);">
<div style="padding: 16px; border-bottom: 1px solid var(--border-subtle);">
<div style="display: flex; justify-content: space-between; margin-bottom: 12px;">
<h3 style="font-family: var(--font-title); font-size: 14px; text-transform: uppercase;">Доступные аккаунты</h3>
<span id="available-accounts-count" class="text-muted" style="font-size: 12px;">Список ещё не получен</span>
</div>
<div class="input-group">
<input type="text" id="routing-account-search" placeholder="Поиск по аккаунтам, ролям или моделям..." style="width: 100%;">
</div>
<div style="display: flex; gap: 8px; margin-top: 8px;">
<select id="routing-filter-provider" style="flex:1;"><option value="all">Все провайдеры</option></select>
<select id="routing-filter-status" style="flex:1;"><option value="all">Все статусы</option></select>
</div>
</div>
<div class="available-accounts-list" id="routing-available-container" style="flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px;">
<!-- Rendered by app.js -->
</div>
<div style="padding: 16px; text-align: center; font-size: 12px; color: var(--text-muted); border-top: 1px dashed var(--border-subtle);">
<i class="fa-solid fa-arrows-up-down-left-right"></i> Перетащите аккаунт в нужную роль или нажмите «+» для добавления
</div>
</div>
</div>
</section>
<!-- SKILLS VIEW -->
<section id="view-skills" class="view-pane">
<div class="toolbar">
<div class="search-box">
<span class="search-icon">🔍</span>
<input type="text" id="skills-search" placeholder="Поиск по скиллам, тегам, описанию или агентам...">
</div>
<div class="filters-row">
<select id="filter-skills-source" class="select-filter">
<option value="all">Все источники</option>
</select>
<select id="filter-skills-status" class="select-filter">
<option value="all">Все статусы</option>
<option value="valid">Валидные (Doctor OK)</option>
<option value="invalid">С ошибками</option>
<option value="assigned">Назначенные субагентам</option>
<option value="unassigned">Не назначенные</option>
</select>
<button class="btn btn-secondary btn-sm" id="btn-refresh-skills">↻ Обновить скиллы</button>
<div class="toolbar-stats" id="skills-stats-summary">
Поиск скиллов...
</div>
</div>
</div>
<div class="section-card" style="margin-top:12px;">
<div class="section-card-header" style="display:flex; justify-content:space-between; align-items:center;">
<div>
<div class="section-card-title">Реестр навыков (Agent Skills · SKILL.md)</div>
<div class="section-card-subtitle">Обнаруженные файлы SKILL.md, валидация триггеров и назначение субагентам</div>
</div>
<div id="skills-usage-summary-badge" class="badge">Н/Д: вызовы со скиллами ещё не регистрировались</div>
</div>
<div id="skills-cards-container" class="skills-grid" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap:16px; padding-top:12px;">
<!-- Rendered by app.js -->
</div>
</div>
</section>
<!-- 4. ANALYTICS VIEW (P0-1 & P0-5) -->
<section id="view-analytics" class="view-pane">
<div class="view-header-note">
Телеметрия вызовов и отказоустойчивости за 24 часа. Метрики отражают реальные замеры задержки и частоты ошибок при маршрутизации.
</div>
<div class="overview-grid">
<div class="kpi-card">
<div class="kpi-label">Всего вызовов (24ч)</div>
<div class="kpi-value text-accent" id="analytics-total-calls"></div>
<div class="kpi-sub" id="analytics-calls-breakdown"></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Доля отказов / ошибок</div>
<div class="kpi-value" id="analytics-error-rate"></div>
<div class="kpi-sub" id="analytics-error-rate-sub"></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Латентность (p50 / p95 / max)</div>
<div class="kpi-value text-info" id="analytics-latency-p50"></div>
<div class="kpi-sub" id="analytics-latency-sub"></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Расход токенов</div>
<div class="kpi-value text-muted" id="analytics-tokens-total">Н</div>
<div class="kpi-sub" id="analytics-tokens-sub">Н/Д: провайдеры не отдают токены</div>
</div>
</div>
<div class="analytics-workspace"><section class="section-card"><h2 class="section-card-title">Использование токенов</h2><div id="analytics-token-chart" class="metric-chart">Н/Д: снапшот ещё не получен</div></section><section class="section-card"><h2 class="section-card-title">Запросы по ролям</h2><div id="analytics-role-chart" class="metric-chart">Н/Д: снапшот ещё не получен</div></section><aside class="section-card"><h2 class="section-card-title">Окно измерений</h2><p class="inspector-value">Последние 24 часа</p><p class="inspector-value">Источник: telemetry из /api/snapshot. Произвольный период API не предоставляет.</p><button class="btn btn-secondary" id="analytics-refresh">Обновить</button></aside></div><div class="analytics-tables"><div class="section-card">
<div class="section-card-header">
<div class="section-card-title">Телеметрия по провайдерам ИИ</div>
<div class="section-card-subtitle">Распределение запросов, отказов и задержек в окне измерений</div>
</div>
<div class="table-responsive" id="analytics-providers-table">
<!-- Rendered by app.js -->
</div>
</div>
<div class="section-card">
<div class="section-card-header">
<div class="section-card-title">Телеметрия по ролям агентов</div>
<div class="section-card-subtitle">Нагрузка по ролям команды, частота сбоев и тайминги</div>
</div>
<div class="table-responsive" id="analytics-roles-table">
<!-- Rendered by app.js -->
</div>
</div>
</div></section>
<!-- 7. HEALTH VIEW (P0-2) -->
<section id="view-health" class="view-pane">
<div class="readiness-banner" id="health-readiness-banner">
<!-- Readiness state banner rendered by app.js -->
</div>
<div class="section-card" id="health-preflight-card">
<div class="section-card-header" style="display:flex; justify-content:space-between; align-items:center;">
<div>
<div class="section-card-title">Проверка готовности (Preflight / Dependency Agent)</div>
<div class="section-card-subtitle">Zero-quota аудит окружения, зависимостей, прав доступа и локальных серверов</div>
</div>
<button class="btn btn-secondary btn-sm" id="btn-run-preflight">Запустить проверку готовности</button>
</div>
<div id="preflight-results-container" style="padding-top:12px;">
<!-- Rendered by app.js -->
</div>
</div>
<div class="section-card">
<div class="section-card-header">
<div class="section-card-title">Ресурсы хост-системы</div>
<div class="section-card-subtitle">Мониторинг CPU, памяти, диска и сетевых параметров</div>
</div>
<div class="host-resources-grid" id="health-host-resources">
<!-- Rendered by app.js -->
</div>
</div>
<div class="section-card">
<div class="section-card-header">
<div class="section-card-title">Предупреждения и диагностика</div>
<div class="section-card-subtitle">Анализ рисков, отсутствие резервов и деградация маршрутов</div>
</div>
<div class="warnings-list" id="health-warnings-list">
<!-- Rendered by app.js -->
</div>
</div>
<div id="health-workspace" class="health-workspace"></div>
</section>
<!-- 8. LOGS / EVENTS VIEW (P0-3) -->
<section id="view-logs" class="view-pane">
<div class="toolbar">
<div class="search-box">
<span class="search-icon">🔍</span>
<input type="text" id="logs-search" placeholder="Поиск по событиям или деталям...">
</div>
<div class="filters-row">
<select id="logs-filter-level" class="select-filter">
<option value="all">Все уровни</option>
<option value="info">INFO</option>
<option value="success">SUCCESS</option>
<option value="warning">WARNING</option>
<option value="error">ERROR</option>
</select>
<select id="logs-filter-category" class="select-filter">
<option value="all">Все категории</option>
<option value="workflow_started">Запуск workflow</option><option value="agent_started">Запуск агента</option><option value="agent_completed">Завершение агента</option><option value="workflow_failed">Ошибка workflow</option>
</select>
<button class="btn btn-secondary btn-sm" id="btn-refresh-logs">↻ Обновить журнал</button>
</div>
</div>
<div class="section-card" style="margin-top:12px;">
<div class="section-card-header">
<div class="section-card-title">Журнал событий Hermes Hub</div>
<div class="section-card-subtitle">События workflow из текущего снапшота · до 60 последних записей</div>
</div>
<div class="logs-layout"><div class="logs-container" id="logs-container">
<!-- Workflow events from HubSnapshot -->
</div><aside id="log-detail" class="log-detail"><h2>Детали события</h2><p>Выберите событие в таблице.</p></aside></div>
</div>
</section>
<!-- 9. SETTINGS VIEW (P0-4) -->
<section id="view-settings" class="view-pane">
<!-- Hub Server Settings -->
<div class="settings-card">
<h2 class="settings-group-title">Параметры сервера Hermes Hub</h2>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Хост и порт веб-сервера</div>
<div class="setting-desc">Сетевой адрес и порт для HTTP/REST API и веб-интерфейса</div>
</div>
<div class="setting-control" style="display:flex; gap:8px;">
<input type="text" id="setting-server-host" class="input-text" style="width:140px;" placeholder="127.0.0.1">
<input type="number" id="setting-server-port" class="input-text" style="width:90px;" placeholder="5800">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Токен безопасности сервера</div>
<div class="setting-desc">Обязателен при внешнем подключении (host != 127.0.0.1)</div>
</div>
<div class="setting-control" style="display:flex; align-items:center; gap:8px;">
<span id="setting-token-status-badge" class="badge">Токен не задан</span>
<input type="password" id="setting-server-token-input" class="input-text" placeholder="Задать новый токен...">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<label class="setting-label" for="setting-provider-proxy-url">Прокси / VPN для провайдеров</label>
<div class="setting-desc">Google отказывает по местоположению: «not currently available in your location». Выход через разрешённую страну снимает отказ. Пример: <code>socks5://127.0.0.1:1080</code>. Пусто — без прокси. Отдельному аккаунту можно задать свой адрес.</div>
</div>
<div class="setting-control"><input id="setting-provider-proxy-url" class="input-text" type="text" placeholder="Н/Д: загрузка" aria-label="Адрес прокси для обращений провайдеров"></div>
</div>
<div class="setting-row">
<div class="setting-info">
<label class="setting-label" for="setting-account-check-interval">Проверка аккаунтов и моделей</label>
<div class="setting-desc">Каждые 300 секунд по умолчанию. Проверка отправляет короткий запрос модели и может расходовать квоту.</div>
</div>
<div class="setting-control"><input id="setting-account-check-interval" class="input-text" type="number" min="60" disabled placeholder="Н/Д: загрузка" aria-label="Интервал проверки аккаунтов, секунды"></div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Интервал обновления квот</div>
<div class="setting-desc">Период фонового опроса квот провайдеров (секунды)</div>
</div>
<div class="setting-control">
<select id="setting-quota-interval" class="select-filter">
<option value="60">60 секунд (1 мин)</option>
<option value="120">120 секунд (2 мин)</option>
<option value="300">300 секунд (5 мин — стандарт)</option>
<option value="600">600 секунд (10 мин)</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Порог остатка квоты</div>
<div class="setting-desc">Уровень остатка квоты аккаунта, при котором срабатывает действие</div>
</div>
<div class="setting-control">
<select id="setting-quota-threshold-percent" class="select-filter">
<option value="5">5%</option>
<option value="10" selected>10% (по умолчанию)</option>
<option value="15">15%</option>
<option value="20">20%</option>
<option value="25">25%</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Действие при низком остатке квоты</div>
<div class="setting-desc">Оповещение в журнале и готовности или автоматическое переключение на резервный аккаунт</div>
</div>
<div class="setting-control">
<select id="setting-quota-threshold-action" class="select-filter">
<option value="notify" selected>Оповещение (notify)</option>
<option value="switch">Переключение на резерв (switch)</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Маскирование почты (PII)</div>
<div class="setting-desc">Уровень маскирования адресов электронной почты в снапшотах и логах</div>
</div>
<div class="setting-control">
<select id="setting-email-masking-mode" class="select-filter">
<option value="none" selected>Без маскирования (по умолчанию)</option>
<option value="partial">Частичное (v***@gmail.com)</option>
<option value="full">Полное (***@***.***)</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Роль по умолчанию (Default Role)</div>
<div class="setting-desc">Целевая роль при отсутствии явного указания роли в запросе Hermes</div>
</div>
<div class="setting-control">
<select id="setting-default-role" class="select-filter">
<option value="manager" selected>Главный менеджер (manager)</option>
<option value="developer-1">Кодер 1 (developer-1)</option>
<option value="developer-2">Кодер 2 (developer-2)</option>
<option value="code-reviewer">Ревьюер кода (code-reviewer)</option>
<option value="researcher">Исследователь (researcher)</option>
<option value="tester">Тестировщик (tester)</option>
<option value="integration-expert">Интегратор (integration-expert)</option>
<option value="security-expert">Эксперт по безопасности (security-expert)</option>
<option value="tech-writer">Технический писатель (tech-writer)</option>
<option value="analyst">Аналитик требований (analyst)</option>
<option value="dependency-agent">Агент зависимостей (dependency-agent)</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Тема оформления</div>
<div class="setting-desc">Цветовая схема интерфейса Hub</div>
</div>
<div class="setting-control">
<select id="setting-theme" class="select-filter">
<option value="system">Системная (по умолчанию)</option>
<option value="dark">Тёмная (Dark)</option>
<!-- Medium была полностью реализована в style.css, но в этот
список не попала, и выбрать её было нельзя. Брендбук
требует три темы, причём Medium — отдельная, а не
осветлённая Dark: тёмно-зелёный холст #1A2A1F со
светлыми кремовыми карточками #F7F1E3. Проверено
подстановкой data-theme вручную: тема отрисовывается
верно и отличается от Dark и по фону, и по карточкам. -->
<option value="medium">Средняя (Medium)</option>
<option value="light">Светлая (Light)</option>
</select>
</div>
</div>
<div class="settings-actions">
<button class="btn btn-primary" id="btn-save-hub-settings">Сохранить настройки сервера</button>
</div>
</div>
<!-- Obsidian Shared Memory Settings -->
<div class="settings-card" style="margin-top:16px;" id="settings-obsidian-card">
<div class="section-card-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
<div>
<h2 class="settings-group-title" style="margin:0;">Единая память (Obsidian Vault)</h2>
<div class="setting-desc">Централизованная база знаний и долгосрочная память агентов сервера (/srv/projects/AI-Memory)</div>
</div>
<span id="obsidian-vault-status-badge" class="badge">Проверка...</span>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Путь к хранилищу Obsidian</div>
<div class="setting-desc">Каталог Markdown-хранилища, содержащий папку <code>.obsidian</code> (/srv/projects/AI-Memory)</div>
</div>
<div class="setting-control" style="display:flex; gap:8px; width:45%;">
<input type="text" id="setting-obsidian-vault-path" class="input-text" style="flex:1;" placeholder="/srv/projects/AI-Memory">
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Каноническая структура памяти</div>
<div class="setting-desc" id="obsidian-vault-status-desc">00_SYSTEM, 01_PROJECTS/hermes-hub, 03_LESSONS, 04_PATTERNS, 05_AGENTS, worklog/</div>
</div>
<div class="setting-control" style="display:flex; gap:8px;">
<button class="btn btn-secondary btn-sm" id="btn-check-obsidian-vault">Проверить хранилище</button>
<button class="btn btn-primary btn-sm" id="btn-setup-memory-structure">Настроить структуру памяти</button>
</div>
</div>
<div id="obsidian-vault-details" style="font-size:12px; color:var(--text-muted); margin-top:8px; font-family:var(--font-mono);"></div>
</div>
<!-- Context Compression Settings (A56) -->
<div class="settings-card" style="margin-top:16px;" id="settings-compression-card">
<div class="section-card-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
<div>
<h2 class="settings-group-title" style="margin:0;">Сжатие контекста (Context Compression)</h2>
<div class="setting-desc">Автоматическое сжатие длинного контекста с сохранением 100% технических фактов и сущностей</div>
</div>
<span id="compression-status-badge" class="badge">Н/Д: модель для сжатия не выбрана</span>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Профиль модели для сжатия</div>
<div class="setting-desc">Служебная модель-компрессор (например, Qwen3-4B на порту 8082). Не участвует в маршрутизации Hermes.</div>
</div>
<div class="setting-control">
<select id="setting-compressor-profile" class="select-filter">
<option value="none">Н/Д: модель для сжатия не выбрана (отключено)</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Порог заполнения контекста для запуска сжатия</div>
<div class="setting-desc">Сжатие начинается при превышении процента от измеренного контекста модели (/props)</div>
</div>
<div class="setting-control">
<select id="setting-compression-threshold-percent" class="select-filter">
<option value="50">50% контекста (агрессивное сжатие)</option>
<option value="60">60% контекста</option>
<option value="70">70% контекста</option>
<option value="75" selected>75% контекста (рекомендуется — около 3/4)</option>
<option value="80">80% контекста</option>
<option value="85">85% контекста</option>
<option value="90">90% контекста (позднее сжатие)</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Количество свежих сообщений (Fresh Window)</div>
<div class="setting-desc">Последние сообщения, которые всегда остаются 100% дословными и не подлежат сжатию</div>
</div>
<div class="setting-control">
<select id="setting-compression-keep-recent" class="select-filter">
<option value="2">2 последних сообщения</option>
<option value="3" selected>3 последних сообщения (рекомендуется)</option>
<option value="4">4 последних сообщения</option>
<option value="5">5 последних сообщений</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Диагностика и проверка сжатия</div>
<div class="setting-desc" id="compression-diagnostic-desc">Проверить отклик компрессора и 100% удержание путей, портов, IP и хэшей</div>
</div>
<div class="setting-control" style="display:flex; gap:8px;">
<button class="btn btn-secondary btn-sm" id="btn-check-compression-status">Статус компрессора</button>
<button class="btn btn-primary btn-sm" id="btn-test-compression">Тестовое сжатие (100% фактов)</button>
</div>
</div>
<div id="compression-details-box" style="font-size:12px; color:var(--text-muted); margin-top:8px; font-family:var(--font-mono); white-space:pre-wrap;"></div>
</div>
<!-- Antigravity CLI & Eligibility Settings (A58) -->
<div class="settings-card" style="margin-top:16px;" id="settings-agy-card">
<div class="section-card-header" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
<div>
<h2 class="settings-group-title" style="margin:0;">Параметры Antigravity CLI</h2>
<div class="setting-desc">Версия утилиты, путь к исполняемому файлу и состояние проверки доступности Google</div>
</div>
<span id="agy-eligibility-badge" class="badge">Проверка...</span>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Версия и исполняемый файл agy</div>
<div class="setting-desc" id="agy-version-path-desc">Н/Д: определение...</div>
</div>
<div class="setting-control" style="display:flex; gap:8px;">
<button class="btn btn-secondary btn-sm" id="btn-refresh-agy-eligibility">↻ Проверить состояние</button>
<button class="btn btn-primary btn-sm" id="btn-run-agy-update" title="Обновление agy перезаписывает исполняемый файл и возвращает проверку доступности: сначала выполните обновление, затем повторно примените патч.">⬆ Обновить agy (agy update)</button>
</div>
</div>
<div class="setting-row" style="background:var(--surface-muted); padding:10px 12px; border-radius:var(--radius-sm); border:1px solid var(--border-subtle); margin-top:8px;">
<div class="setting-desc" style="font-size:12px; color:var(--text-secondary); margin:0;">
<strong>Порядок действий:</strong> <em>Обновление agy перезаписывает исполняемый файл и возвращает проверку доступности: сначала выполните обновление, затем повторно примените патч.</em>
</div>
</div>
<div class="setting-row" style="margin-top:12px;">
<div class="setting-info">
<label class="setting-label" for="setting-agy-patch-script-path">Путь к сценарию патча владельца</label>
<div class="setting-desc">Собственный скрипт владельца для снятия проверки доступности (P0-4). Оставьте пустым, если не используется.</div>
</div>
<div class="setting-control" style="display:flex; gap:8px; width:55%;">
<input type="text" id="setting-agy-patch-script-path" class="input-text" style="flex:1;" placeholder="Н/Д: сценарий не указан">
<button class="btn btn-secondary btn-sm" id="btn-run-agy-patch-script">⚡ Запустить сценарий патча</button>
</div>
</div>
<div id="agy-eligibility-details" style="font-size:12px; color:var(--text-muted); margin-top:8px; font-family:var(--font-mono);"></div>
</div>
<!-- Hub Updates -->
<div class="settings-card" style="margin-top:16px;" id="settings-updates-card">
<h2 class="settings-group-title">Обновление Hermes Hub</h2>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Текущая версия и сборка</div>
<div class="setting-desc" id="update-installed-info">Hermes Hub</div>
</div>
<div class="setting-control" style="display:flex; align-items:center; gap:8px;">
<span id="update-status-badge" class="badge">Не проверено</span>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Проверка обновлений</div>
<div class="setting-desc" id="update-last-checked-desc">Последняя проверка: еще не выполнялась</div>
</div>
<div class="setting-control" style="display:flex; gap:8px;">
<button class="btn btn-secondary btn-sm" id="btn-check-updates">Проверить обновления</button>
<button class="btn btn-primary btn-sm" id="btn-apply-update" disabled>Установить обновление</button>
</div>
</div>
<div id="update-details-block" class="update-details-box hidden" style="margin-top:12px; padding:12px; background:var(--surface-muted); border-radius:var(--radius-sm); border:1px solid var(--border-subtle);">
<div id="update-release-title" style="font-weight:600; font-size:13px; margin-bottom:4px;"></div>
<div id="update-release-meta" style="font-size:12px; color:var(--text-muted); margin-bottom:8px;"></div>
<div id="update-release-notes" style="font-size:12px; max-height:160px; overflow-y:auto; white-space:pre-wrap; background:var(--surface-card); padding:8px; border-radius:4px; border:1px solid var(--border-subtle); font-family:var(--font-mono);"></div>
</div>
</div>
<!-- Read-Only System Paths -->
<div class="settings-card" style="margin-top:16px;">
<h2 class="settings-group-title">Системные пути и окружение</h2>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Каталог данных Hermes</div>
<div class="setting-desc mono-path" id="path-hermes-home"></div>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Каталог конфигурации</div>
<div class="setting-desc mono-path" id="path-config-dir"></div>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Файл журнала событий</div>
<div class="setting-desc mono-path" id="path-log-file"></div>
</div>
</div>
</div>
<!-- Client Local Settings -->
<div class="settings-card" style="margin-top:16px;">
<h2 class="settings-group-title">Параметры веб-клиента (Браузер)</h2>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Источник данных в браузере</div>
<div class="setting-desc">Автоматическое переключение между Live API и фикстурой</div>
</div>
<div class="setting-control">
<select id="setting-source-mode" class="select-filter">
<option value="auto">Авто (Live API с fallback)</option>
<option value="live">Строго Live API (/api/snapshot)</option>
<option value="fixture">Строго фикстура (snapshot.example.json)</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Интервал авто-опроса UI</div>
<div class="setting-desc">Частота обновления снимка состояния в окне браузера</div>
</div>
<div class="setting-control">
<select id="setting-poll-interval" class="select-filter">
<option value="3000">3 секунды</option>
<option value="5000" selected>5 секунд (стандарт)</option>
<option value="10000">10 секунд</option>
<option value="0">Отключить авто-опрос</option>
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Локальный токен клиента (X-Hub-Token)</div>
<div class="setting-desc">Токен для заголовка X-Hub-Token при внешних вызовах</div>
</div>
<div class="setting-control">
<input type="password" id="setting-client-token-input" class="input-text" placeholder="Введите токен...">
</div>
</div>
<div class="settings-actions">
<button class="btn btn-secondary" id="btn-save-client-settings">Сохранить локальные параметры</button>
</div>
</div>
<!-- Clean Setup / Reset Configuration (P0-4) -->
<div class="settings-card" style="margin-top:16px; border:1px solid rgba(220, 53, 69, 0.3);">
<h2 class="settings-group-title" style="color:var(--status-error, #e57373);">Сброс конфигурации</h2>
<div class="setting-row">
<div class="setting-info">
<div class="setting-label">Начать настройку заново</div>
<div class="setting-desc">Сбросить все цепочки ролей и список профилей в чистое начальное состояние. Все учетные данные и токены аккаунтов сохраняются.</div>
</div>
<div class="setting-control">
<button class="btn btn-danger" id="btn-reset-router-config">Начать настройку заново</button>
</div>
</div>
</div>
</section>
</div>
</main>
</div>
<!-- Modal Layer -->
<div id="modal-backdrop" class="modal-backdrop hidden">
<div id="modal-container" class="modal-card">
<div class="modal-header">
<h2 id="modal-title" class="modal-title">Модальное окно</h2>
<button id="modal-close-btn" class="modal-close" title="Закрыть"></button>
</div>
<div id="modal-body" class="modal-body">
<!-- Injected dynamically -->
</div>
<div id="modal-footer" class="modal-footer">
<!-- Injected dynamically -->
</div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toast-container" class="toast-container"></div>
<!-- Application Logic -->
<script src="app.js"></script>
<script src="static/workspace.js"></script>
<script src="static/workflow.js"></script>
<script>
const urlParams = new URLSearchParams(window.location.search);
const theme = urlParams.get('theme');
if (theme) {
document.body.setAttribute('data-theme', theme);
const sel = document.getElementById('theme-selector');
if (sel) sel.value = theme;
}
</script>
</body>
</html>