Initial import: WebAisMap

Closes TG-4

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-04 07:56:45 +03:00
commit 03075f1ef1
1460 changed files with 16334 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Установка сертификата — AIS Map</title>
<link rel="icon" href="/svg/icon.svg" type="image/svg+xml"/>
<link rel="shortcut icon" href="/svg/icon.svg"/>
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 620px; margin: 0 auto; padding: 20px; background: #f5f5f5; color: #333; }
h1 { font-size: 22px; text-align: center; }
.card { background: #fff; border-radius: 8px; padding: 18px; margin: 14px 0;
box-shadow: 0 1px 4px rgba(0,0,0,.12); }
.card h2 { font-size: 16px; margin: 0 0 10px; }
.card ol { padding-left: 20px; margin: 0; }
.card li { margin: 6px 0; line-height: 1.5; }
.btn { display: block; width: 100%; text-align: center; padding: 14px;
background: #2196F3; color: #fff; text-decoration: none;
border-radius: 6px; font-size: 16px; font-weight: 600; margin: 10px 0; }
.btn:active { background: #1976D2; }
.note { font-size: 13px; color: #777; text-align: center; margin-top: 16px; }
.back { display: block; text-align: center; margin-top: 18px; color: #2196F3; }
</style>
</head>
<body>
<h1>Установка сертификата AIS Map</h1>
<a class="btn" href="/ca.crt">Скачать сертификат (.crt)</a>
<div class="card">
<h2>Android</h2>
<ol>
<li>Нажмите кнопку выше — скачается файл</li>
<li>Откройте скачанный файл (или: Настройки → Безопасность → Установка сертификатов)</li>
<li>Выберите <b>«CA-сертификат»</b> / <b>«Центр сертификации»</b></li>
<li>Подтвердите установку</li>
<li>Готово — перезагрузите страницу карты</li>
</ol>
</div>
<div class="card">
<h2>iPhone / iPad</h2>
<ol>
<li>Нажмите кнопку выше — откроется «Профиль загружен»</li>
<li>Настройки → Основные → <b>Профили</b> → «AIS Map Root CA» → Установить</li>
<li>Настройки → Основные → <b>Об этом устройстве</b> → Доверие сертификатам → включите «AIS Map Root CA»</li>
<li>Готово — перезагрузите страницу карты</li>
</ol>
</div>
<div class="card">
<h2>Firefox (ПК / Android)</h2>
<ol>
<li>Откройте <a href="/ca.pem">/ca.pem</a></li>
<li>Firefox предложит импортировать — отметьте <b>«Доверять для идентификации сайтов»</b></li>
<li>Нажмите ОК</li>
</ol>
</div>
<div class="card">
<h2>Chrome / Edge (ПК)</h2>
<ol>
<li>Скачайте <a href="/ca.crt">/ca.crt</a></li>
<li>Настройки → Конфиденциальность и безопасность → Безопасность → Управление сертификатами</li>
<li>Вкладка <b>«Доверенные корневые центры»</b> → Импорт → выберите файл</li>
</ol>
</div>
<p class="note">После установки предупреждение о безопасности больше не появится.<br>
Сертификат действителен 10 лет и работает только в вашей локальной сети.</p>
<a class="back" href="/">← Вернуться к карте</a>
</body>
</html>
+700
View File
@@ -0,0 +1,700 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no"/>
<title>AIS Map v2</title>
<link rel="icon" href="/svg/icon.svg" type="image/svg+xml"/>
<link rel="shortcut icon" href="/svg/icon.svg"/>
<link rel="stylesheet" href="/static/leaflet/leaflet.css"/>
<link rel="stylesheet" href="/static/css/style.css"/>
</head>
<body>
<nav id="navbar">
<div class="nav-brand">AIS Map v2 <span class="nav-version">2026-04-21a</span></div>
<button class="hamburger" id="hamburger">&#9776;</button>
<div class="nav-tabs" id="nav-tabs">
<a class="nav-tab active" data-tab="map">Карта</a>
<a class="nav-tab nav-tab--targets" data-tab="targets">Цели</a>
<a class="nav-tab" data-tab="stats">Статистика</a>
<a class="nav-tab" data-tab="settings">Настройки</a>
<a class="nav-tab" data-tab="transponder">Транспондер</a>
<a class="nav-tab" data-tab="logs">Логи</a>
<a class="nav-tab" data-tab="config">Конфиги</a>
<a class="nav-tab" data-tab="console">Консоль</a>
</div>
<div id="conn-banner" class="conn-banner" style="display:none" role="status" aria-live="polite"></div>
</nav>
<!-- Global banners (visible on all tabs/pages) -->
<div id="global-banners" class="global-banners" aria-live="polite">
<div id="danger-banner" class="danger-banner" style="display:none">ВНИМАНИЕ</div>
</div>
<!-- ==================== MAP PAGE ==================== -->
<div id="page-map" class="tab-page active">
<div id="status">Ожидание данных AIS...</div>
<div id="sidebar">
<h3 class="vessel-sidebar-heading">Цели <span class="vessel-count-paren">(<span id="vessel-count">0</span><span id="vessel-count-suffix"></span>)</span></h3>
<div id="vessel-list-toolbar" class="vessel-list-toolbar">
<input type="search" id="vessel-search" class="vessel-search-input" placeholder="Поиск: MMSI, имя, позывной…" autocomplete="off" spellcheck="false"/>
<div class="vessel-toolbar-row">
<label class="vessel-toolbar-label">Сортировка
<select id="vessel-sort" class="vessel-toolbar-select">
<option value="dist-asc">Расстояние ↑</option>
<option value="dist-desc">Расстояние ↓</option>
<option value="time-desc" selected>Обновление (новые)</option>
<option value="time-asc">Обновление (старые)</option>
<option value="name-asc">Название А–Я</option>
<option value="mmsi-asc">MMSI</option>
<option value="class-asc">Класс A/B</option>
<option value="speed-desc">Скорость</option>
</select>
</label>
<label class="vessel-toolbar-label">Класс
<select id="vessel-filter-class" class="vessel-toolbar-select">
<option value="all" selected>Все</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="BS">База</option>
<option value="N">Буи</option>
</select>
</label>
<label class="vessel-toolbar-label">Тип
<select id="vessel-filter-shiptype" class="vessel-toolbar-select">
<option value="all" selected>Все</option>
<option value="unknown">Не указан / 0</option>
<option value="fishing">Рыболов (30)</option>
<option value="tug">Буксир (3132)</option>
<option value="passenger">Пассажир (6069)</option>
<option value="cargo">Грузовой (7079)</option>
<option value="tanker">Танкер (8089)</option>
<option value="other">Прочие</option>
</select>
</label>
</div>
</div>
<div id="vessel-list"></div>
<div id="range-filter">
<div id="range-label">Радиус: <span id="range-value">&#8734;</span></div>
<input type="range" id="range-slider" min="0" max="100" value="100"/>
</div>
</div>
<div id="cursor-coords">Координаты: -</div>
<div id="ownship-panel">
<h4>Своё судно</h4>
<div class="row"><span class="label">Источник:</span> <span id="os-source">-</span></div>
<div class="row"><span class="label">Координаты:</span> <span id="os-coords">-</span></div>
<div class="row"><span class="label">Курс:</span> <span id="os-course">-</span></div>
<div class="row"><span class="label">Скорость:</span> <span id="os-speed">-</span></div>
<div class="row"><span class="label">Спутники:</span> <span id="os-sats">-</span></div>
<div class="row os-compass-row">
<span class="label">Компас:</span>
<span class="os-compass">
<button class="compass-toggle" id="btn-rotate-map" type="button" title="Вращать карту по компасу">
<span class="compass-toggle-dot" aria-hidden="true"></span>
</button>
<span class="compass-dial" id="os-compass-dial" title="Север">
<span class="compass-arrow" id="os-compass-arrow" aria-hidden="true">
<svg viewBox="0 0 64 64" width="28" height="28" focusable="false" aria-hidden="true">
<path d="M32 4 L42 34 L32 28 L22 34 Z" fill="#ff4d4d"/>
<path d="M32 60 L42 30 L32 36 L22 30 Z" fill="#c9d1d9" opacity="0.9"/>
<circle cx="32" cy="32" r="3.2" fill="#0d1117" opacity="0.9"/>
</svg>
</span>
<span class="compass-n">N</span>
</span>
</span>
</div>
<div class="os-source-hint" id="os-source-hint" style="margin-top:5px;font-size:10px;color:#667">Источник GPS и «Следовать» — в панели управления картой</div>
</div>
<div id="map"></div>
<!-- Map control panel (centre, north-up, ruler, one-hand toggle) -->
<div id="map-controls" class="map-controls" role="toolbar" aria-label="Управление картой">
<button type="button" class="map-ctrl" id="mc-zoom-in" title="Приблизить" aria-label="Приблизить">+</button>
<button type="button" class="map-ctrl" id="mc-zoom-out" title="Отдалить" aria-label="Отдалить"></button>
<button type="button" class="map-ctrl" id="mc-center" title="Центрировать на своём судне" aria-label="Центрировать на своём судне">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><circle cx="12" cy="12" r="3" fill="currentColor"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" fill="none"/></svg>
</button>
<button type="button" class="map-ctrl" id="mc-north-up" title="Сброс вращения (Север вверх)" aria-label="Север вверх">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 3 L17 20 L12 16 L7 20 Z" fill="#ff4d4d" stroke="#000" stroke-width=".5"/><text x="12" y="11" text-anchor="middle" font-size="7" font-weight="900" fill="#fff">N</text></svg>
</button>
<button type="button" class="map-ctrl" id="mc-compass" title="Heading-up: вращать карту по компасу" aria-label="Вращать карту по компасу">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.4" fill="none"/>
<path d="M12 4 L14.5 12 L12 10 L9.5 12 Z" fill="#ff4d4d"/>
<path d="M12 20 L14.5 12 L12 14 L9.5 12 Z" fill="currentColor" opacity=".55"/>
<text x="12" y="3.6" text-anchor="middle" font-size="3.6" font-weight="900" fill="currentColor">N</text>
</svg>
</button>
<button type="button" class="map-ctrl" id="mc-ruler" title="Линейка (рулетка)" aria-label="Линейка">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M2 15 L15 2 L22 9 L9 22 Z M5 16 L8 13 M7 18 L10 15 M9 20 L12 17 M11 14 L14 11 M13 16 L16 13 M15 8 L18 5" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg>
</button>
<button type="button" class="map-ctrl" id="mc-follow" title="Следовать за судном (навигатор: авто-центрирование + авто-зум по скорости)" aria-label="Режим следования">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path d="M12 2 L18 20 L12 17 L6 20 Z" fill="currentColor" stroke="#000" stroke-width=".4"/>
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.4" fill="none" stroke-dasharray="2 3" opacity=".6"/>
</svg>
</button>
<button type="button" class="map-ctrl" id="mc-gps-src" title="Источник GPS: внутренний (NMEA) / телефон" aria-label="Источник GPS" data-src="nmea">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" class="mc-gps-icon-nmea"><rect x="3" y="4" width="18" height="12" rx="2" stroke="currentColor" stroke-width="1.6" fill="none"/><path d="M8 20h8M12 16v4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><circle cx="9" cy="10" r="1.3" fill="currentColor"/><path d="M13 8h5M13 11h4M13 13h3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" class="mc-gps-icon-phone" style="display:none"><rect x="7" y="2" width="10" height="20" rx="2" stroke="currentColor" stroke-width="1.6" fill="none"/><circle cx="12" cy="19" r="1" fill="currentColor"/><path d="M10 6h4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity=".7"/><circle cx="12" cy="12" r="1.6" fill="currentColor"/><path d="M9.5 12a2.5 2.5 0 0 1 5 0M8 12a4 4 0 0 1 8 0" stroke="currentColor" stroke-width="1.2" fill="none"/></svg>
</button>
<button type="button" class="map-ctrl" id="mc-onehand" title="Одноручный режим (крупные кнопки + тап-зум)" aria-label="Одноручный режим">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M9 3v8H7c-1.1 0-2 .9-2 2v2l4 6h8c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2h-5V3c0-1.1-.9-2-2-2S9 1.9 9 3z" stroke="currentColor" stroke-width="1.4" fill="none"/></svg>
</button>
</div>
<!-- One-hand side pad (hidden until toggled) -->
<div id="onehand-pad" class="onehand-pad" hidden aria-hidden="true">
<button type="button" class="onehand-btn" id="oh-zoom-in" aria-label="Приблизить">+</button>
<button type="button" class="onehand-btn" id="oh-zoom-out" aria-label="Отдалить"></button>
<button type="button" class="onehand-btn" id="oh-center" aria-label="Центрировать">
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><circle cx="12" cy="12" r="3" fill="currentColor"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>
</button>
</div>
<!-- Vessel infowindow (MarineTraffic-style: desktop floating + draggable, mobile bottom-sheet) -->
<div id="vessel-infowindow" class="vinf" hidden aria-hidden="true" role="dialog"></div>
<!-- Semi-transparent corner HUD:
- Own ship readout (SOG / coords / compass)
- Selected target (with X to clear) OR top-5 nearest targets (minimal info)
Replaces the mobile tab-bar for nearest vessels. -->
<div id="nearby-hud" class="nearby-hud" aria-label="Навигационный дисплей">
<div class="nearby-hud__own" id="nhud-own">
<div class="nhud-own__row">
<span class="nhud-compass" id="nhud-compass" title="Компас (GPS-курс или Android-компас)">
<svg viewBox="0 0 40 40" width="36" height="36" aria-hidden="true">
<circle cx="20" cy="20" r="18" fill="rgba(0,0,0,.35)" stroke="rgba(210,255,26,.35)" stroke-width="1"/>
<text x="20" y="7.8" text-anchor="middle" font-size="5" font-weight="700" fill="#d2ff1a">N</text>
<text x="20" y="36" text-anchor="middle" font-size="5" font-weight="500" fill="#8b949e">S</text>
<text x="35" y="22" text-anchor="middle" font-size="5" font-weight="500" fill="#8b949e">E</text>
<text x="5" y="22" text-anchor="middle" font-size="5" font-weight="500" fill="#8b949e">W</text>
<g class="nhud-compass-needle" id="nhud-needle" style="transform-origin:20px 20px">
<path d="M20 5 L23 20 L20 18 L17 20 Z" fill="#ff4d4d"/>
<path d="M20 35 L23 20 L20 22 L17 20 Z" fill="rgba(201,209,217,.85)"/>
</g>
</svg>
<span class="nhud-compass-val" id="nhud-compass-val"></span>
</span>
<div class="nhud-own__info">
<div class="nhud-own__line"><span class="nhud-k">SOG</span><b id="nhud-own-sog"></b></div>
<div class="nhud-own__line"><span class="nhud-k">COG</span><b id="nhud-own-cog"></b></div>
<div class="nhud-own__coords" id="nhud-own-coords"></div>
<div class="nhud-own__src" id="nhud-own-src"></div>
</div>
</div>
</div>
<div class="nearby-hud__list" id="nhud-list" aria-label="Ближайшие цели"></div>
<button type="button" class="nearby-hud__toggle" id="nhud-toggle" title="Свернуть/развернуть" aria-label="Свернуть/развернуть">
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M3 6 L8 11 L13 6" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<!--
Mobile bottom tab bar — removed in favour of the always-on #nearby-hud corner overlay
and the full "Цели" tab in the navbar. Left commented-out in case we ever want to
re-introduce tab-style bottom navigation.
<div id="mob-panel-bar">
<button class="mob-panel-tab" data-panel="sidebar" id="mob-tab-targets">Ближайшие</button>
<button class="mob-panel-tab" data-panel="ownship" id="mob-tab-ownship">Судно</button>
</div>
-->
</div>
<!-- ==================== TARGETS PAGE (full list, primarily for mobile) ==================== -->
<div id="page-targets" class="tab-page">
<div id="targets-page-inner" class="targets-page-inner">
<h3 class="vessel-sidebar-heading">Цели <span class="vessel-count-paren">(<span id="targets-count">0</span><span id="targets-count-suffix"></span>)</span></h3>
<div id="targets-toolbar" class="vessel-list-toolbar">
<input type="search" id="targets-search" class="vessel-search-input" placeholder="Поиск: MMSI, имя, позывной…" autocomplete="off" spellcheck="false"/>
<div class="vessel-toolbar-row">
<label class="vessel-toolbar-label">Сортировка
<select id="targets-sort" class="vessel-toolbar-select">
<option value="dist-asc">Расстояние ↑</option>
<option value="dist-desc">Расстояние ↓</option>
<option value="time-desc" selected>Обновление (новые)</option>
<option value="time-asc">Обновление (старые)</option>
<option value="name-asc">Название А–Я</option>
<option value="mmsi-asc">MMSI</option>
<option value="class-asc">Класс A/B</option>
<option value="speed-desc">Скорость</option>
</select>
</label>
<label class="vessel-toolbar-label">Класс
<select id="targets-filter-class" class="vessel-toolbar-select">
<option value="all" selected>Все</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="BS">База</option>
<option value="N">Буи</option>
</select>
</label>
<label class="vessel-toolbar-label">Тип
<select id="targets-filter-shiptype" class="vessel-toolbar-select">
<option value="all" selected>Все</option>
<option value="unknown">Не указан / 0</option>
<option value="fishing">Рыболов (30)</option>
<option value="tug">Буксир (3132)</option>
<option value="passenger">Пассажир (6069)</option>
<option value="cargo">Грузовой (7079)</option>
<option value="tanker">Танкер (8089)</option>
<option value="other">Прочие</option>
</select>
</label>
</div>
</div>
<div id="targets-list" class="targets-list"></div>
</div>
</div>
<!-- ==================== STATS PAGE ==================== -->
<div id="page-stats" class="tab-page">
<div class="stats-grid">
<div class="stat-card"><div class="val" id="st-active">0</div><div class="lbl">Активных целей</div></div>
<div class="stat-card"><div class="val" id="st-total">0</div><div class="lbl">Всего за сессию</div></div>
<div class="stat-card"><div class="val" id="st-class-a">0</div><div class="lbl">Класс A</div></div>
<div class="stat-card"><div class="val" id="st-class-b">0</div><div class="lbl">Класс B</div></div>
<div class="stat-card"><div class="val" id="st-ais-msg">0</div><div class="lbl">AIS сообщений</div></div>
<div class="stat-card"><div class="val" id="st-gps-msg">0</div><div class="lbl">GPS сообщений</div></div>
<div class="stat-card"><div class="val" id="st-gps-fix">-</div><div class="lbl">GPS фикс</div></div>
<div class="stat-card"><div class="val" id="st-uptime">-</div><div class="lbl">Аптайм приложения</div></div>
<div class="stat-card"><div class="val" id="st-sys-uptime">-</div><div class="lbl">Аптайм системы</div></div>
<div class="stat-card"><div class="val" id="st-rx-time">-</div><div class="lbl">Время приёмника</div></div>
<div class="stat-card"><div class="val" id="st-cpu-temp">-</div><div class="lbl">Температура CPU</div></div>
<div class="stat-card"><div class="val" id="st-cpu-load">-</div><div class="lbl">Загрузка CPU</div></div>
<div class="stat-card"><div class="val" id="st-mem">-</div><div class="lbl">Память</div></div>
<div class="stat-card"><div class="val" id="st-ais-rate">-</div><div class="lbl">AIS поток</div></div>
<div class="stat-card"><div class="val" id="st-nmea-rate">-</div><div class="lbl">Все NMEA</div></div>
<div class="stat-card stat-card-test"><div class="val" id="st-test-mmsi">0</div><div class="lbl">Тест (247320161)</div><div class="test-ch-detail"><span>A: <b id="st-test-mmsi-a">0</b></span> <span>B: <b id="st-test-mmsi-b">0</b></span></div></div>
</div>
<div class="stat-section">
<h3>AIS сообщения по типам</h3>
<table class="stat-table">
<thead><tr><th>Тип</th><th>Описание</th><th>Количество</th></tr></thead>
<tbody id="st-ais-types"></tbody>
</table>
</div>
<div class="stat-section">
<details class="stat-details" id="st-adv-details">
<summary>Расширенная статистика (ошибки/парсинг)</summary>
<div class="stat-adv-grid">
<div class="stat-adv-card">
<div class="stat-adv-title">Ошибки парсинга</div>
<table class="stat-table stat-table-compact">
<tbody id="st-parse-errors"></tbody>
</table>
</div>
<div class="stat-adv-card">
<div class="stat-adv-title">UDP / фрагменты</div>
<table class="stat-table stat-table-compact">
<tbody id="st-udp-errors"></tbody>
</table>
</div>
<div class="stat-adv-card">
<div class="stat-adv-title">Хранилище / отправка</div>
<table class="stat-table stat-table-compact">
<tbody id="st-storage-errors"></tbody>
</table>
</div>
<div class="stat-adv-card">
<div class="stat-adv-title">WS / прочее</div>
<table class="stat-table stat-table-compact">
<tbody id="st-misc-counters"></tbody>
</table>
</div>
</div>
</details>
</div>
<div class="slots-section">
<div class="slots-header" id="slots-toggle">
<span class="slots-arrow" id="slots-arrow">&#9654;</span> Слоты TDMA
</div>
<div class="slots-content" id="slots-content">
<div class="slots-channels">
<div class="slots-channel">
<div class="slots-ch-title">Канал A</div>
<div class="slots-info" id="slots-info-a"><span class="slots-no-data">Нет данных</span></div>
<div class="slots-bar" id="slots-bar-a" style="display:none"><div class="slots-bar-fill" id="slots-bar-fill-a"></div></div>
<canvas id="slots-rssi-a" class="rssi-chart" width="482" height="90" style="display:none"></canvas>
<div class="slots-canvas-wrap" id="slots-wrap-a" style="display:none">
<canvas id="slots-canvas-a" width="482" height="180"></canvas>
</div>
</div>
<div class="slots-channel">
<div class="slots-ch-title">Канал B</div>
<div class="slots-info" id="slots-info-b"><span class="slots-no-data">Нет данных</span></div>
<div class="slots-bar" id="slots-bar-b" style="display:none"><div class="slots-bar-fill" id="slots-bar-fill-b"></div></div>
<canvas id="slots-rssi-b" class="rssi-chart" width="482" height="90" style="display:none"></canvas>
<div class="slots-canvas-wrap" id="slots-wrap-b" style="display:none">
<canvas id="slots-canvas-b" width="482" height="180"></canvas>
</div>
</div>
</div>
<div class="slots-legend">
<span class="slots-legend-item"><span class="slots-legend-box free"></span> Свободен</span>
<span class="slots-legend-item"><span class="slots-legend-box occ"></span> Занят</span>
<span class="slots-legend-item" style="margin-left:12px"><span style="display:inline-block;width:16px;height:2px;background:#c678dd;vertical-align:middle;margin-right:4px"></span> RSSI</span>
<span class="slots-legend-item"><span style="display:inline-block;width:2px;height:12px;background:#ffd166;vertical-align:middle;margin-right:4px"></span> Event</span>
<span class="slots-legend-item"><span style="display:inline-block;width:2px;height:12px;background:#3fb950;vertical-align:middle;margin-right:4px;opacity:.9"></span> Кадр</span>
<span class="slots-legend-item" style="margin-left:12px"><span style="display:inline-block;width:16px;height:2px;background:#4fc3f7;vertical-align:middle;margin-right:4px"></span> Noise Floor</span>
<span class="slots-legend-item"><span style="display:inline-block;width:16px;height:2px;background:#f0883e;vertical-align:middle;margin-right:4px"></span> Threshold</span>
</div>
<div class="slots-test-send">
<span class="slots-test-label">Тест слота:</span>
<select id="test-slot-channel" class="slots-test-input" style="width:auto;min-width:80px">
<option value="A">Канал A</option>
<option value="B">Канал B</option>
</select>
<input type="number" id="test-slot-number" class="slots-test-input" min="0" max="2249" value="0" placeholder="Слот 0-2249" style="width:90px"/>
<button id="test-slot-send" class="slots-test-btn">Отправить</button>
<span id="test-slot-status" class="slots-test-status"></span>
</div>
<div id="slot-selected-info" class="slots-selected-info">Клик по слоту покажет детали.</div>
</div>
<div class="slots-tooltip" id="slots-tooltip"></div>
</div>
</div>
<!-- ==================== SETTINGS PAGE ==================== -->
<div id="page-settings" class="tab-page">
<!-- Network mode -->
<div class="settings-card">
<h3>Сеть / Режим работы</h3>
<div class="settings-row">
<span class="settings-label">Текущий режим</span>
<span id="net-live-mode" class="net-status unknown">...</span>
</div>
<div class="settings-row">
<span class="settings-label">IP-адрес устройства</span>
<span class="settings-value" id="net-live-ip">-</span>
</div>
<div class="settings-row">
<span class="settings-label">SSID</span>
<span class="settings-value" id="net-live-ssid">-</span>
</div>
<div class="net-mode-btns">
<div class="net-mode-btn" id="net-btn-ap" data-mode="ap">Точка доступа (AP)</div>
<div class="net-mode-btn" id="net-btn-wifi" data-mode="wifi">WiFi-клиент</div>
</div>
<!-- AP settings -->
<div id="net-ap-fields" style="display:none">
<div class="settings-row"><span class="settings-label">SSID точки доступа</span><input class="net-input" id="net-ap-ssid" placeholder="AISMap"/></div>
<div class="settings-row"><span class="settings-label">Пароль AP</span><input class="net-input" id="net-ap-psk" type="text" placeholder="aismap1234"/></div>
<div class="settings-row"><span class="settings-label">IP точки доступа</span><input class="net-input" id="net-ap-ip" placeholder="192.168.4.1/24"/></div>
</div>
<!-- WiFi settings -->
<div id="net-wifi-fields" style="display:none">
<div class="settings-row"><span class="settings-label">SSID сети</span>
<span style="display:flex;gap:6px;align-items:center">
<input class="net-input" id="net-wifi-ssid" placeholder="MyNetwork"/>
<button class="net-btn net-btn-primary" id="net-scan-btn" style="padding:6px 12px;font-size:11px">Сканировать</button>
</span>
</div>
<div id="net-scan-list" class="net-scan-list" style="display:none"></div>
<div class="settings-row"><span class="settings-label">Пароль WiFi</span><input class="net-input" id="net-wifi-psk" type="password" placeholder="********"/></div>
<div class="settings-row"><span class="settings-label">Статический IP</span><input class="net-input" id="net-wifi-ip" placeholder="192.168.22.50/24"/></div>
<span class="net-toggle-adv" id="net-adv-toggle">Дополнительно &#9662;</span>
<div class="net-advanced" id="net-advanced">
<div class="settings-row"><span class="settings-label">Шлюз</span><input class="net-input" id="net-wifi-gw" placeholder="192.168.22.1"/></div>
<div class="settings-row"><span class="settings-label">DNS</span><input class="net-input" id="net-wifi-dns" placeholder="8.8.8.8"/></div>
<div class="settings-row"><span class="settings-label">Интерфейс</span><input class="net-input" id="net-iface" placeholder="wlan0"/></div>
</div>
</div>
<div style="margin-top:14px;display:flex;gap:8px">
<button class="net-btn net-btn-primary" id="net-save-btn">Сохранить настройки</button>
<button class="net-btn net-btn-danger" id="net-switch-btn">Применить и переключить</button>
</div>
<div id="net-msg" class="net-msg"></div>
</div>
<div class="settings-card">
<h3>Подключение</h3>
<div class="settings-row"><span class="settings-label">UDP порт (AIS + GPS)</span><span class="settings-value">5006</span></div>
<div class="settings-row"><span class="settings-label">Web-сервер</span><span class="settings-value" id="set-server">-</span></div>
<div class="settings-row"><span class="settings-label">HTTPS</span><span class="settings-value" id="set-https">-</span></div>
</div>
<div class="settings-card">
<h3>Тайминги целей</h3>
<div class="settings-row">
<span class="settings-label">Потеря цели (подсветка), мин</span>
<span style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:flex-end">
<input class="net-input" type="number" id="set-losing-target-min" min="1" step="1" style="width:120px"/>
<span class="settings-value" id="set-losing-target-preview">-</span>
</span>
</div>
<div class="settings-row">
<span class="settings-label">Удаление цели, мин</span>
<span style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:flex-end">
<input class="net-input" type="number" id="set-remove-target-min" min="1" step="1" style="width:120px"/>
<span class="settings-value" id="set-remove-target-preview">-</span>
</span>
</div>
</div>
<div class="settings-card">
<h3>Единицы измерения</h3>
<div class="settings-row">
<span class="settings-label">Расстояние</span>
<select id="set-dist-unit" class="net-input" style="width:auto;min-width:100px">
<option value="nm">Морские мили (NM)</option>
<option value="km">Километры (км)</option>
<option value="au">Астрономические единицы (AU)</option>
</select>
</div>
<div class="settings-row">
<span class="settings-label">Скорость</span>
<select id="set-speed-unit" class="net-input" style="width:auto;min-width:100px">
<option value="kn">Узлы (уз.)</option>
<option value="kmh">Километры в час (км/ч)</option>
</select>
</div>
</div>
<div class="settings-card">
<h3>Радиусы пеленга</h3>
<div class="settings-row">
<span class="settings-label">Радиус 1 (ВНИМАНИЕ), <span id="set-warn-radius-unit">NM</span></span>
<span style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:flex-end">
<input type="range" id="set-warn-radius-slider" min="0" max="50" step="0.1" value="0" style="width:220px"/>
<input class="net-input" type="number" id="set-warn-radius" min="0" step="0.1" placeholder="0 = выкл" style="width:120px"/>
</span>
</div>
<div class="settings-row">
<span class="settings-label">Радиус 2 (подсветка), <span id="set-near-radius-unit">NM</span></span>
<span style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:flex-end">
<input type="range" id="set-near-radius-slider" min="0" max="50" step="0.1" value="0" style="width:220px"/>
<input class="net-input" type="number" id="set-near-radius" min="0" step="0.1" placeholder="0 = выкл" style="width:120px"/>
</span>
</div>
<p class="transponder-hint" style="margin:8px 0 0">Окружности рисуются вокруг “Своё судно”. Радиус 1 включает баннер «ВНИМАНИЕ», радиус 2 подсвечивает близкие цели.</p>
</div>
<div class="settings-card">
<h3>Сертификат</h3>
<div class="settings-row"><span class="settings-label">Статус CA</span><span class="settings-value"><a href="/cert" style="color:#4fc3f7">Установить сертификат</a></span></div>
</div>
<div class="settings-card">
<h3>О системе</h3>
<div class="settings-row"><span class="settings-label">Версия</span><span class="settings-value">1.0</span></div>
<div class="settings-row"><span class="settings-label">Secure context</span><span class="settings-value" id="set-secure">-</span></div>
</div>
</div>
<!-- ==================== TRANSPONDER (Class B) ==================== -->
<div id="page-transponder" class="tab-page">
<div class="settings-card">
<h3>Наше судно (Class B / B+)</h3>
<p class="transponder-hint">MMSI, имя, позывной, габариты и данные для сообщений 18, 19, 24. Движение из GPS при включённой опции ниже.</p>
<div class="settings-row"><span class="settings-label">MMSI</span><input class="net-input" type="number" id="tp-mmsi" min="0"/></div>
<div class="settings-row"><span class="settings-label">Имя судна (до 20)</span><input class="net-input" id="tp-shipname" maxlength="20"/></div>
<div class="settings-row"><span class="settings-label">Позывной</span><input class="net-input" id="tp-callsign" maxlength="7"/></div>
<div class="settings-row ship-type-row"><span class="settings-label">Тип судна</span>
<select class="net-input ship-type-select" id="tp-ship-type" title="ITU-R M.1371-6, табл. 51 (099)"></select>
</div>
<p class="transponder-hint ship-type-legend">Табл. 51: <abbr title="dangerous goods">ОПГ</abbr> — опасные грузы; <abbr title="materials hazardous only in bulk">ОН</abbr> — опасные только насыпью; <abbr title="harmful substances">ВВ</abbr> — вредные вещества; <abbr title="marine pollutants">МЗ</abbr> — морские загрязнители. Категории X, Y, Z, OS — по ИМО (ранее A, B, C, D).</p>
<div class="settings-row"><span class="settings-label">Габариты: A B C D (м)</span>
<span style="display:flex;flex-wrap:wrap;gap:6px;align-items:center">
<span class="settings-value" style="font-size:10px;color:#667;margin-right:4px">A нос</span>
<input class="net-input tp-dim" type="number" id="tp-to-bow" min="0" max="511" title="A: нос → GPS, м (≤511)"/>
<span class="settings-value" style="font-size:10px;color:#667">B корма</span>
<input class="net-input tp-dim" type="number" id="tp-to-stern" min="0" max="511" title="B: GPS → корма, м"/>
<span class="settings-value" style="font-size:10px;color:#667">C порт</span>
<input class="net-input tp-dim" type="number" id="tp-to-port" min="0" max="63" title="C: порт → GPS, м (≤63)"/>
<span class="settings-value" style="font-size:10px;color:#667">D штб</span>
<input class="net-input tp-dim" type="number" id="tp-to-starboard" min="0" max="63" title="D: GPS → штюрборд, м"/>
</span>
</div>
<div class="ship-editor-block">
<p class="transponder-hint" style="margin-top:0">Мышью: <strong>корма</strong> — L, <strong>правый борт</strong> — W (доли A/L и C/W как в начале жеста), <strong>GPS</strong> — сдвиг внутри L×W. Увеличивать W/L можно и за пределами контура — координаты не обрезаются по краю корпуса. Точные A–D — в полях выше.</p>
<div class="ship-editor-wrap">
<svg id="ship-editor-svg" class="ship-editor-svg" viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg" aria-label="Редактор габаритов судна">
<defs>
<marker id="ship-dim-arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
<path d="M10,5 L0,0 L0,10 Z" fill="#8b949e"/>
</marker>
</defs>
<g id="ship-editor-inner" transform="translate(0,0)">
<g id="ship-dim-beam" class="ship-dim-group" aria-hidden="true"></g>
<g id="ship-dim-length" class="ship-dim-group" aria-hidden="true"></g>
<path id="ship-hull" class="ship-hull" d=""/>
<line id="ship-grid-h" class="ship-grid" x1="0" y1="0" x2="0" y2="0"/>
<line id="ship-grid-v" class="ship-grid" x1="0" y1="0" x2="0" y2="0"/>
<text id="ship-lbl-bow" class="ship-axis-lbl ship-axis-lbl--dyn" x="0" y="0" text-anchor="middle">нос (курс)</text>
<text id="ship-lbl-stern" class="ship-axis-lbl ship-axis-lbl--dyn" x="0" y="0" text-anchor="middle">корма</text>
<g id="ship-gps-marker" class="ship-gps-group" transform="translate(0,0)">
<circle class="ship-gps-hit" r="16" cx="0" cy="0"/>
<circle class="ship-gps-dot" r="9" cx="0" cy="0"/>
<text class="ship-gps-lbl" x="0" y="4" text-anchor="middle">GPS</text>
</g>
<g id="ship-edge-handles" class="ship-edge-handles" aria-hidden="true">
<g class="ship-handle ship-handle--stern" data-ship-edge="stern"><circle r="11" cx="0" cy="0"/></g>
<g class="ship-handle ship-handle--starboard" data-ship-edge="starboard"><circle r="11" cx="0" cy="0"/></g>
</g>
</g>
</svg>
<ul class="ship-editor-legend">
<li><strong>Серые размеры</strong> — W сверху, L справа</li>
<li><strong>Жёлтые маркеры</strong> — корма = L, правый борт = W (A–D пересчитываются пропорционально)</li>
<li><strong>GPS</strong> — перетаскивание на схеме или спинбоксы A–D</li>
<li><strong>A</strong> — нос → GPS (бит 21–29)</li>
<li><strong>B</strong> — GPS → корма (1220)</li>
<li><strong>C</strong> — порт → GPS (611)</li>
<li><strong>D</strong> — GPS → штюрборд (0–5)</li>
</ul>
</div>
</div>
<div class="settings-row"><span class="settings-label">Vendor ID (6)</span><input class="net-input" id="tp-vendorid" maxlength="6" placeholder="1HZZZZ"/></div>
<div class="settings-row"><span class="settings-label">Модель / серийный №</span>
<span style="display:flex;gap:6px">
<input class="net-input" type="number" id="tp-model" min="0"/>
<input class="net-input" type="number" id="tp-serial" min="0"/>
</span>
</div>
<div class="settings-row"><span class="settings-label">Движение из GPS</span><label><input type="checkbox" id="tp-use-gps"/> курс/скорость/heading из ownship</label></div>
<div class="settings-row"><span class="settings-label">GPS (ownship)</span><span class="settings-value" id="tp-ownship-hint"></span></div>
</div>
<div class="settings-card">
<h3>Отправка NRZI</h3>
<p class="transponder-hint">В UDP <code>127.0.0.1:6010</code> уходит пакет как в «Тест слота»: <strong>канал</strong> (1 байт) + <strong>слот</strong> (uint16 LE) + сгенерированный NRZI.</p>
<div class="settings-row"><span class="settings-label">Канал / слот</span>
<span style="display:flex;flex-wrap:wrap;gap:8px;align-items:center">
<select id="tp-slot-channel" class="slots-test-input" style="width:auto;min-width:80px">
<option value="A">Канал A</option>
<option value="B">Канал B</option>
</select>
<input type="number" id="tp-slot-number" class="slots-test-input" min="0" max="2249" value="0" title="Слот 02249" style="width:90px"/>
</span>
</div>
<div class="settings-row"><span class="settings-label">Кодер NRZI</span>
<select id="tp-nrzi-encoder" class="net-input" style="width:auto;min-width:220px">
<option value="aistx" selected>ais_nrzi_pipeline/phy.py (gr-aistx) — по умолчанию</option>
<option value="ais_phy">ais_phy.py (pyais + CRC без reverse-byte)</option>
</select>
</div>
<div class="settings-row"><span class="settings-label">NRZI упаковка</span>
<select id="tp-nrzi-mode" class="net-input" style="width:auto">
<option value="packed">packed (8 бит/байт)</option>
<option value="expanded">expanded (1 бит = 0x00/0xFF)</option>
</select>
</div>
<div class="settings-row"><span class="settings-label">Преамбула NRZI</span><label><input type="checkbox" id="tp-preamble"/> чередование 10… перед флагом (как gr-aistx)</label></div>
<div class="settings-row"><span class="settings-label">Длина преамбулы (бит)</span><input class="net-input" type="number" id="tp-preamble-bits" min="0" max="128" value="24" title="ITU AIS training ~24 бит; 0 — без преамбулы при включённой галочке не рекомендуется"/></div>
<div class="settings-row"><span class="settings-label">Добор payload до октета</span><label><input type="checkbox" id="tp-pad-payload"/> нулями до кратности 8 бит перед CRC (как в GNU Radio)</label></div>
<div class="settings-row"><span class="settings-label">Добор NRZ до (бит)</span><input class="net-input" type="number" id="tp-pad-nrz-bits" min="0" max="4096" value="256" title="0=выкл. Нули в NRZ перед NRZI, если кадр короче (как padd_frame). В GR было 200; с преамбулой 24+HDLC часто &gt;200 бит — тогда нужно ≥256, иначе добор не добавляет биты."/></div>
<div class="settings-row"><span class="settings-label">Импульс TX (GPIO)</span>
<span style="display:flex;flex-wrap:wrap;gap:10px;align-items:center;max-width:640px">
<label><input type="checkbox" id="tp-gpio-auto"/> после UDP: пауза 50–100 ms, затем скрипт (PTT)</label>
</span>
</div>
<div class="settings-row"><span class="settings-label">Пауза перед GPIO (мс)</span><input class="net-input" type="number" id="tp-gpio-delay-ms" min="50" max="100" value="75" title="После отправки UDP, перед запуском pulse-скрипта"/></div>
<div class="settings-row"><span class="settings-label">Скрипт импульса</span><input class="net-input" id="tp-gpio-script" style="max-width:480px" placeholder="/root/pulse_once.py или путь к scripts/pulse_once.py"/></div>
<div style="margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center">
<button type="button" class="net-btn net-btn-primary" id="tp-save">Сохранить</button>
<button type="button" class="net-btn" id="tp-preview">Обновить превью</button>
<button type="button" class="net-btn" id="tp-gpio-pulse-once" title="Только импульс, без UDP">Импульс TX</button>
<button type="button" class="net-btn" id="tp-send-18">Отпр. 18</button>
<button type="button" class="net-btn" id="tp-send-19">Отпр. 19</button>
<button type="button" class="net-btn" id="tp-send-24a">Отпр. 24 ч.1 (имя)</button>
<button type="button" class="net-btn" id="tp-send-24b">Отпр. 24 ч.2 (статика)</button>
<button type="button" class="net-btn net-btn-danger" id="tp-send-broadcast">Бродкаст 18+19+24A+24B</button>
</div>
<div id="tp-msg" class="net-msg"></div>
</div>
<div class="settings-card">
<h3>Сырой NRZI (hex)</h3>
<p class="transponder-hint">Вставьте байты в hex (пробелы допускаются). Отправка: те же <strong>канал</strong> и <strong>слот</strong>, что выше → UDP <code>127.0.0.1:6010</code> как у теста слота. При включённой галочке «после UDP» к телу запроса подмешиваются настройки GPIO из формы.</p>
<textarea id="tp-raw-hex" class="transponder-raw-hex" rows="4" spellcheck="false" placeholder="66 66 66 fe … или cc cc cc fe …"></textarea>
<div style="margin-top:10px">
<button type="button" class="net-btn net-btn-primary" id="tp-send-raw">Отправить сырой NRZI</button>
</div>
</div>
<div class="settings-card">
<h3>Превью (чистый NRZI и полный UDP-кадр)</h3>
<pre class="transponder-preview" id="tp-preview-out">Нажмите «Обновить превью».</pre>
</div>
</div>
<!-- ==================== LOGS PAGE ==================== -->
<div id="page-logs" class="tab-page">
<div class="logs-toolbar">
<label>Фильтр:</label>
<select id="log-filter">
<option value="all">Все</option>
<option value="ais">AIS</option>
<option value="gps">GPS</option>
<option value="unknown">Прочее</option>
</select>
<input type="text" id="log-search" placeholder="Поиск..." style="width:120px"/>
<span class="log-btn active" id="log-autoscroll">Автопрокрутка</span>
<span class="log-btn" id="log-clear">Очистить</span>
<span style="flex:1"></span>
<span id="log-count" style="font-size:11px;color:#667">0 строк</span>
</div>
<div id="log-output"></div>
</div>
<!-- ==================== CONFIG PAGE ==================== -->
<div id="page-config" class="tab-page">
<div class="config-toolbar">
<span id="cfg-title">Конфиги</span>
<span style="flex:1"></span>
<span class="config-svc-label">Сервис:</span>
<span id="cfg-svc-state" class="config-svc-badge unknown">...</span>
</div>
<div class="config-body">
<div class="config-tabs">
<button class="config-tab active" id="cfg-tab-mini" type="button">AIS-catcher Mini</button>
<button class="config-tab" id="cfg-tab-aishub" type="button">AisHub</button>
<span style="flex:1"></span>
<span id="cfg-file-hint" class="config-file-hint">Файл: /ais-mini.conf</span>
</div>
<textarea id="cfg-editor" class="config-editor" spellcheck="false" placeholder="Загрузка..."></textarea>
<div class="config-actions">
<button class="net-btn net-btn-primary" id="cfg-save-btn">Сохранить конфиг</button>
<button class="net-btn net-btn-danger" id="cfg-restart-btn">Перезапустить сервис</button>
<button class="net-btn" id="cfg-reload-btn" style="background:#30363d;color:#e0e0e0">Обновить</button>
<span style="flex:1"></span>
<span id="cfg-msg" class="config-msg"></span>
</div>
<div class="config-hint" id="cfg-bottom-hint">Файл: /ais-mini.conf &nbsp;|&nbsp; Сервис: aisMini.service</div>
</div>
</div>
<!-- ==================== CONSOLE PAGE ==================== -->
<div id="page-console" class="tab-page">
<div class="console-toolbar">
<span>Локальная оболочка на устройстве</span>
<span style="flex:1"></span>
<span id="console-status"></span>
</div>
<div id="terminal-unavailable"></div>
<div id="terminal-wrap"></div>
</div>
<script>
// Service Worker: кеш тайлов и ассетов. Регистрируем в корне, чтобы scope='/'.
if ('serviceWorker' in navigator && location.protocol !== 'file:') {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(function (e) {
try { console.warn('[AISMap] SW register failed:', e); } catch (_) {}
});
});
}
</script>
<script src="/static/leaflet/leaflet.js"></script>
<script src="/static/leaflet/leaflet-rotate.js"></script>
<script src="/static/leaflet/Leaflet.VectorGrid.bundled.min.js"></script>
<script src="/static/js/ship_dims_editor.js"></script>
<script src="/static/js/ship_types_table51.js"></script>
<!-- Cache-bust app.js so field deployments pick up the latest UI logic -->
<script src="/static/js/app.js?v=2026-04-30h"></script>
</body>
</html>