You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

709 lines
33 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Радиотрекинг</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
<link rel="stylesheet" href="/static/styles.css?v=20260319r2" />
</head>
<body>
<div class="bg-glow bg-glow-a"></div>
<div class="bg-glow bg-glow-b"></div>
<div id="auth-overlay" class="auth-overlay auth-overlay-hidden">
<div class="auth-dialog card">
<h2>Вход в систему</h2>
<p class="muted">Авторизация выполняется через Keycloak.</p>
<label class="field-control">
<span class="field-label">Логин</span>
<input id="login-username" type="text" autocomplete="username" />
</label>
<label class="field-control">
<span class="field-label">Пароль</span>
<input id="login-password" type="password" autocomplete="current-password" />
</label>
<div class="editor-actions">
<button id="login-submit" class="btn btn-primary" type="button">Войти</button>
<span id="login-state" class="badge">авторизация: ожидание</span>
</div>
</div>
</div>
<main id="app-shell" class="app-shell">
<aside id="side-nav" class="side-nav card">
<div class="nav-head">
<button id="menu-toggle" class="btn menu-toggle" type="button" aria-controls="menu-list" aria-expanded="true">Свернуть меню</button>
<div class="auth-summary">
<span id="auth-user-chip" class="badge badge-meta">пользователь: гость</span>
<span id="auth-role-chip" class="badge badge-meta">роль: -</span>
<button id="logout-button" class="btn btn-compact" type="button">Выйти</button>
</div>
</div>
<div class="menu-wrap">
<div id="menu-list" class="menu-list">
<section class="menu-group" data-menu-group="monitoring">
<button
class="menu-group-toggle"
type="button"
data-menu-group-toggle="monitoring"
aria-controls="menu-group-monitoring"
aria-expanded="true"
>
<span class="menu-group-title">Система</span>
<span id="menu-badge-monitoring" class="menu-group-badge">н/д</span>
</button>
<div id="menu-group-monitoring" class="menu-group-body">
<button class="menu-item menu-item-active" data-section="overview" type="button">
<span class="menu-item-icon" aria-hidden="true">O</span>
<span class="menu-item-text">Обзор</span>
<span class="menu-item-note">Статус</span>
</button>
<button class="menu-item" data-section="frequencies" type="button">
<span class="menu-item-icon" aria-hidden="true">F</span>
<span class="menu-item-text">Частоты</span>
<span class="menu-item-note">Таблица</span>
</button>
</div>
</section>
<section class="menu-group" data-menu-group="io">
<button
class="menu-group-toggle"
type="button"
data-menu-group-toggle="io"
aria-controls="menu-group-io"
aria-expanded="true"
>
<span class="menu-group-title">Сигнал</span>
<span id="menu-badge-io" class="menu-group-badge">н/д</span>
</button>
<div id="menu-group-io" class="menu-group-body">
<button class="menu-item" data-section="io" type="button">
<span class="menu-item-icon" aria-hidden="true">I</span>
<span class="menu-item-text">Сигнал</span>
<span class="menu-item-note">Обмен</span>
</button>
<button class="menu-item" data-section="history" type="button">
<span class="menu-item-icon" aria-hidden="true">H</span>
<span class="menu-item-text">История</span>
<span class="menu-item-note">Журнал</span>
</button>
</div>
</section>
<section class="menu-group" data-menu-group="config">
<button
class="menu-group-toggle"
type="button"
data-menu-group-toggle="config"
aria-controls="menu-group-config"
aria-expanded="true"
>
<span class="menu-group-title">Настройки</span>
<span id="menu-badge-config" class="menu-group-badge">н/д</span>
</button>
<div id="menu-group-config" class="menu-group-body">
<button class="menu-item" data-section="servers" type="button">
<span class="menu-item-icon" aria-hidden="true">S</span>
<span class="menu-item-text">Серверы</span>
<span class="menu-item-note">Узлы</span>
</button>
<button class="menu-item" data-section="json" type="button">
<span class="menu-item-icon" aria-hidden="true">{}</span>
<span class="menu-item-text">Конфиг</span>
<span class="menu-item-note">Файл</span>
</button>
</div>
</section>
</div>
</div>
<div class="side-meta">
<div class="datetime-panel-controls">
<button
id="datetime-toggle"
class="btn btn-compact"
type="button"
aria-controls="meta-panel"
aria-expanded="true"
>
Скрыть панель
</button>
<button id="density-toggle" class="btn btn-compact" type="button">Вид: детальный</button>
</div>
<div id="meta-panel" class="meta-panel">
<div id="date-time-panel" class="date-time-panel">
<span id="updated-date" class="meta-pill">дата: н/д</span>
<span id="updated-time" class="meta-pill">время: н/д</span>
</div>
<div class="status-panel">
<span id="health-status" class="meta-pill">сервис: н/д</span>
<span id="delivery-status" class="meta-pill">доставка: н/д</span>
</div>
<label class="timezone-picker">
<span>часовой пояс</span>
<select id="timezone-select"></select>
</label>
</div>
</div>
</aside>
<section class="content-area">
<section id="section-overview" class="panel panel-active">
<header class="hero card overview-hero">
<h2>Радиотрекинг</h2>
<p class="muted">Координаты по RSSI и частотам.</p>
<div class="hero-actions">
<button id="refresh-now" class="btn btn-primary">Обновить</button>
<button id="toggle-auto-refresh" class="btn" type="button">Пауза автообновления</button>
<label class="refresh-interval-control" for="auto-refresh-seconds">
<span>Интервал, с</span>
<input id="auto-refresh-seconds" type="number" min="1" max="120" step="1" value="2" />
</label>
<span id="refresh-state" class="badge badge-meta">автообновление: вкл (2с)</span>
</div>
</header>
<div class="overview-layout">
<article class="card overview-position-card">
<h2>Координаты</h2>
<div class="result-box">
<div><span class="muted">Частота:</span> <b id="selected-freq">-</b></div>
<div><span class="muted">X:</span> <b id="pos-x">-</b></div>
<div><span class="muted">Y:</span> <b id="pos-y">-</b></div>
<div><span class="muted">Z:</span> <b id="pos-z">-</b></div>
<div><span class="muted">Ошибка:</span> <b id="rmse">-</b></div>
</div>
</article>
<article class="card monitor-board overview-monitor-card">
<h2>Статус</h2>
<div class="monitor-headline">
<span class="io-chip io-chip-neutral">Сервис: <b id="ov-health-chip">н/д</b></span>
<span class="io-chip io-chip-neutral">Доставка: <b id="ov-delivery-chip">н/д</b></span>
<span class="io-chip io-chip-neutral">Обновлено: <b id="ov-updated-at">н/д</b></span>
</div>
<div class="monitor-grid">
<section class="monitor-panel monitor-kpi-panel">
<div class="overview-metrics">
<div class="metric-tile">
<span class="metric-title">Входы онлайн</span>
<b id="ov-input-online" class="metric-value">0/0</b>
</div>
<div class="metric-tile">
<span class="metric-title">Выходы онлайн</span>
<b id="ov-output-online" class="metric-value">0/0</b>
</div>
<div class="metric-tile">
<span class="metric-title">События</span>
<b id="ov-history-total" class="metric-value">0</b>
</div>
<div class="metric-tile">
<span class="metric-title">Успех отправки</span>
<b id="ov-success-rate" class="metric-value">0%</b>
</div>
</div>
</section>
<section class="monitor-panel monitor-flow-panel">
<h3>Потоки</h3>
<div class="monitor-flow-row">
<span>Входные потоки</span>
<b id="ov-input-online-bar-text">0/0</b>
</div>
<div class="monitor-progress">
<span id="ov-input-online-bar" style="width:0%"></span>
</div>
<div class="monitor-flow-row">
<span>Выходные потоки</span>
<b id="ov-output-online-bar-text">0/0</b>
</div>
<div class="monitor-progress">
<span id="ov-output-online-bar" style="width:0%"></span>
</div>
<div class="monitor-flow-row">
<span>Успех отправки</span>
<b id="ov-delivery-bar-text">0%</b>
</div>
<div class="monitor-progress monitor-progress-accent">
<span id="ov-delivery-bar" style="width:0%"></span>
</div>
</section>
</div>
</article>
</div>
</section>
<section id="section-frequencies" class="panel">
<article class="card">
<h2>Решения по частотам</h2>
<div class="table-wrap">
<table id="freq-table">
<thead>
<tr>
<th>Частота (МГц)</th>
<th>X</th>
<th>Y</th>
<th>Z</th>
<th>СКО (RMSE)</th>
<th>Точно</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</article>
<article class="card monitor-board">
<h2>Аналитика</h2>
<div class="monitor-grid">
<section class="monitor-panel monitor-frequency-panel">
<h3>Диапазон</h3>
<div id="ov-frequency-health" class="frequency-health-list"></div>
</section>
<section class="monitor-panel monitor-topfreq-panel">
<h3>Лучшие частоты</h3>
<div id="ov-top-frequencies" class="top-frequencies"></div>
</section>
<section class="monitor-panel monitor-trends-panel">
<h3>Точность</h3>
<div class="trend-stack">
<article class="trend-card">
<div class="trend-head">
<span>RMSE, м</span>
<b id="ov-trend-rmse-meta">н/д</b>
</div>
<div id="ov-trend-rmse-chart" class="sparkline-wrap"></div>
</article>
</div>
</section>
</div>
</article>
</section>
<section id="section-io" class="panel">
<article class="card">
<h2>Сигнал и отправка</h2>
<p class="muted">Приём данных и отправка координат.</p>
<div class="io-grid">
<section class="io-block">
<h3>Приёмники</h3>
<div id="input-flow" class="io-list"></div>
</section>
<section class="io-block">
<h3>Отправка</h3>
<div id="output-flow" class="io-list"></div>
</section>
</div>
<div id="io-admin-controls">
<h3 class="io-history-title">Тестовые сбои</h3>
<div id="error-controls" class="io-list"></div>
</div>
</article>
<article class="card monitor-board">
<h2>Обработка</h2>
<div class="monitor-grid">
<section class="monitor-panel monitor-signal-panel">
<h3>Сигналы</h3>
<div id="ov-signal-grid" class="signal-grid"></div>
</section>
<section class="monitor-panel monitor-stage-panel">
<h3>Этапы</h3>
<div id="ov-pipeline-stages" class="pipeline-stages"></div>
</section>
<section class="monitor-panel monitor-trends-panel">
<h3>Тренды</h3>
<div class="trend-stack">
<article class="trend-card">
<div class="trend-head">
<span>Средний RSSI, дБм</span>
<b id="ov-trend-rssi-meta">н/д</b>
</div>
<div id="ov-trend-rssi-chart" class="sparkline-wrap"></div>
</article>
<article class="trend-card">
<div class="trend-head">
<span>Успех отправки, %</span>
<b id="ov-trend-delivery-meta">н/д</b>
</div>
<div id="ov-trend-delivery-chart" class="sparkline-wrap"></div>
</article>
</div>
</section>
</div>
</article>
</section>
<section id="section-history" class="panel">
<article class="card history-dashboard history-head-card">
<h2>Журнал обмена</h2>
<p class="muted">Приём, расчёт и отправка результатов.</p>
</article>
<div class="history-layout">
<article class="card history-data-card">
<h2>Журнал</h2>
<div class="history-toolbar">
<label>
Статус
<select id="history-filter">
<option value="all">Все</option>
<option value="ok">Ок</option>
<option value="error">Ошибка</option>
<option value="partial">Частично</option>
<option value="skipped">Пропущено</option>
<option value="disabled">Отключено</option>
<option value="warming_up">Прогрев</option>
</select>
</label>
<div class="history-toolbar-right">
<label>
От
<input id="history-date-from" type="datetime-local" />
</label>
<label>
До
<input id="history-date-to" type="datetime-local" />
</label>
<label>
Страница
<select id="history-page-size">
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
</select>
</label>
<div class="history-pager">
<button id="history-prev" class="btn" type="button">Назад</button>
<span id="history-page-info" class="badge badge-meta">Стр. 1/1 • 0 записей</span>
<button id="history-next" class="btn" type="button">Вперёд</button>
</div>
<button id="history-date-reset" class="btn" type="button">Сброс времени</button>
<div id="history-admin-actions" class="history-admin-actions">
<button id="history-record-toggle" class="btn" type="button">Пауза записи</button>
<span id="history-record-state" class="badge badge-meta">запись: вкл</span>
<button id="clear-history" class="btn" type="button">Очистить историю</button>
</div>
</div>
</div>
<div class="table-wrap history-table-wrap">
<table id="io-history-table">
<thead>
<tr>
<th>Время</th>
<th>Частота (МГц)</th>
<th>Вход</th>
<th>Выход</th>
<th>Статус</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</article>
<article class="card history-monitor-card">
<h2>Сводка</h2>
<div class="history-kpis">
<div class="kpi-card">
<span class="kpi-title">Событий</span>
<b id="hist-total" class="kpi-value">0</b>
</div>
<div class="kpi-card">
<span class="kpi-title">Успешно</span>
<b id="hist-ok" class="kpi-value">0</b>
</div>
<div class="kpi-card">
<span class="kpi-title">Проблемы</span>
<b id="hist-problem" class="kpi-value">0</b>
</div>
<div class="kpi-card">
<span class="kpi-title">Частот</span>
<b id="hist-freqs" class="kpi-value">0</b>
</div>
<div class="kpi-card">
<span class="kpi-title">Последнее</span>
<b id="hist-last" class="kpi-value">н/д</b>
</div>
</div>
<div class="history-insights">
<section class="insight-panel">
<h3>Диагностика</h3>
<div id="history-monitor" class="history-monitor"></div>
</section>
<section class="insight-panel">
<h3>Тренды</h3>
<div id="history-trends" class="history-trends"></div>
</section>
</div>
</article>
</div>
</section>
<section id="section-servers" class="panel">
<article class="card servers-head-card">
<h2>Узлы системы</h2>
<p class="muted">Приёмники, фильтр и адреса отправки.</p>
</article>
<div class="servers-layout servers-layout-modern">
<article class="card servers-card servers-card-modern">
<div class="server-card-head">
<h3 class="servers-title">Приёмники</h3>
<p class="muted">Адреса, частоты и координаты.</p>
</div>
<div class="server-card-body">
<div class="selector-row">
<label class="field-control">
<span class="field-label">Активный вход</span>
<select id="receiver-select"></select>
</label>
<span id="receiver-count" class="badge badge-meta chip-counter">входов: 0</span>
</div>
<div class="action-group">
<button id="add-receiver" class="btn" type="button">Добавить вход</button>
<button id="remove-receiver" class="btn" type="button">Удалить вход</button>
</div>
<div class="field-grid field-grid-2">
<label class="field-control">
<span class="field-label">Имя</span>
<input id="rx-id" type="text" placeholder="rx_north" />
</label>
<label class="field-control">
<span class="field-label">URL</span>
<input id="rx-url" type="text" placeholder="http://receiver-r0:9000/data" />
</label>
</div>
<label class="field-control">
<span class="field-label">Частоты, МГц</span>
<input id="rx-frequencies" type="text" placeholder="433.92, 868.1, 915.0" />
<span class="field-hint">Укажите через запятую только рабочие частоты этого входа.</span>
</label>
<div class="field-grid field-grid-3">
<label class="field-control">
<span class="field-label">X</span>
<input id="rx-center-x" type="number" step="0.001" />
</label>
<label class="field-control">
<span class="field-label">Y</span>
<input id="rx-center-y" type="number" step="0.001" />
</label>
<label class="field-control">
<span class="field-label">Z</span>
<input id="rx-center-z" type="number" step="0.001" />
</label>
</div>
</div>
</article>
<article class="card servers-card servers-card-modern">
<div class="server-card-head">
<h3 class="servers-title">Общий фильтр</h3>
<p class="muted">Применяется автоматически ко всем входным серверам.</p>
</div>
<div class="server-card-body">
<label class="field-control">
<span class="field-label">Фильтр включен</span>
<select id="shared-filter-enabled">
<option value="true">да</option>
<option value="false">нет</option>
</select>
</label>
<div class="field-grid field-grid-2">
<label class="field-control">
<span class="field-label">Мин. частота, МГц</span>
<input id="shared-min-freq" type="number" step="0.001" min="0" />
</label>
<label class="field-control">
<span class="field-label">Макс. частота, МГц</span>
<input id="shared-max-freq" type="number" step="0.001" min="0" />
</label>
</div>
<div class="field-grid field-grid-2">
<label class="field-control">
<span class="field-label">Мин. RSSI, дБм</span>
<input id="shared-min-rssi" type="number" step="0.1" />
</label>
<label class="field-control">
<span class="field-label">Макс. RSSI, дБм</span>
<input id="shared-max-rssi" type="number" step="0.1" />
</label>
</div>
</div>
</article>
<article class="card servers-card servers-card-modern">
<div class="server-card-head">
<h3 class="servers-title">Серверы отправки</h3>
<p class="muted">Адреса для передачи координат.</p>
</div>
<div class="server-card-body">
<div class="selector-row">
<label class="field-control">
<span class="field-label">Активный выход</span>
<select id="output-select"></select>
</label>
<span id="output-count" class="badge badge-meta chip-counter">выходов: 0</span>
</div>
<div class="action-group">
<button id="add-output-server" class="btn" type="button">Добавить выход</button>
<button id="remove-output-server" class="btn" type="button">Удалить выход</button>
</div>
<div class="field-grid field-grid-2">
<label class="field-control">
<span class="field-label">Имя</span>
<input id="out-name" type="text" placeholder="sink_main" />
</label>
<label class="field-control">
<span class="field-label">Адрес</span>
<input id="out-ip" type="text" placeholder="output-sink:8080" />
</label>
</div>
<label class="field-control">
<span class="field-label">API-токен</span>
<input id="write-token" type="password" placeholder="необязательно" />
</label>
</div>
</article>
</div>
<article class="card servers-actions-card">
<div class="editor-actions">
<button id="load-servers" class="btn">Загрузить</button>
<button id="save-servers" class="btn btn-primary">Сохранить узлы</button>
<span id="servers-state" class="badge">узлы: н/д</span>
</div>
</article>
<article id="users-card" class="card servers-actions-card">
<div class="server-card-head">
<h3 class="servers-title">Пользователи</h3>
<p class="muted">Управление учётными записями и ролями Keycloak.</p>
</div>
<div class="users-layout">
<div class="table-wrap users-table-wrap">
<table id="users-table">
<thead>
<tr>
<th>Логин</th>
<th>Роль</th>
<th>Состояние</th>
<th>Имя</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="users-form">
<label class="field-control">
<span class="field-label">ID пользователя</span>
<input id="user-id" type="text" placeholder="заполняется автоматически" />
</label>
<label class="field-control">
<span class="field-label">Логин</span>
<input id="user-username" type="text" placeholder="operator_1" />
</label>
<div class="field-grid field-grid-2">
<label class="field-control">
<span class="field-label">Имя</span>
<input id="user-first-name" type="text" />
</label>
<label class="field-control">
<span class="field-label">Фамилия</span>
<input id="user-last-name" type="text" />
</label>
</div>
<div class="field-grid field-grid-2">
<label class="field-control">
<span class="field-label">Роль</span>
<select id="user-role">
<option value="user">Пользователь</option>
<option value="admin">Администратор</option>
</select>
</label>
<label class="field-control">
<span class="field-label">Включён</span>
<select id="user-enabled">
<option value="true">Да</option>
<option value="false">Нет</option>
</select>
</label>
</div>
<label class="field-control">
<span class="field-label">Пароль</span>
<input id="user-password" type="password" placeholder="для создания или смены" />
</label>
<div class="editor-actions">
<button id="load-users" class="btn" type="button">Обновить список</button>
<button id="create-user" class="btn btn-primary" type="button">Создать</button>
<button id="update-user" class="btn" type="button">Сохранить</button>
<button id="reset-user-password" class="btn" type="button">Сменить пароль</button>
<button id="delete-user" class="btn" type="button">Удалить</button>
<button id="clear-user-form" class="btn" type="button">Очистить</button>
</div>
<span id="users-state" class="badge">пользователи: н/д</span>
</div>
</div>
</article>
</section>
<section id="section-json" class="panel">
<article class="card config-head-card">
<h2>Конфиг</h2>
<div class="editor-actions">
<button id="load-config" class="btn">Загрузить</button>
<button id="save-config" class="btn btn-primary">Сохранить файл</button>
<span id="config-state" class="badge">конфиг: н/д</span>
</div>
</article>
<div class="config-layout config-layout-modern">
<article class="card config-editor-card config-editor-modern">
<div class="config-section-head">
<h3>JSON</h3>
<p class="muted">Полный файл настроек.</p>
</div>
<div class="config-editor-shell">
<div class="editor-toolbar">
<span class="editor-chip">JSON</span>
<span class="editor-chip">UTF-8</span>
<span class="editor-chip">runtime + input + system</span>
</div>
<textarea id="config-editor" class="editor" spellcheck="false"></textarea>
</div>
</article>
<article class="card config-help-card config-help-modern">
<div class="config-section-head">
<h3>Структура</h3>
<p class="muted">Основные разделы конфигурации.</p>
</div>
<div class="config-hints config-hints-grid">
<p><b>input.receivers[]</b><br />приемники: координаты, URL и частоты.</p>
<p><b>runtime.output_servers[]</b><br />серверы отправки координат.</p>
<p><b>input.default_input_filter</b><br />общий фильтр частот и RSSI.</p>
<p><b>system</b><br />системные таймеры, лимиты и автообновление.</p>
</div>
<h3>Подсказки</h3>
<ul class="config-tips">
<li>Поддерживайте уникальные `receiver_id` для каждого входа.</li>
<li>Согласуйте диапазоны частот между входными серверами.</li>
<li>Перед сохранением проверяйте JSON на валидность.</li>
</ul>
</article>
</div>
</section>
</section>
</main>
<script src="/static/app.js?v=20260319r2"></script>
<div id="toast-container" class="toast-container" aria-live="polite" aria-atomic="true"></div>
</body>
</html>