Files
WebAisMap/templates/index.html
T
Grigo 03075f1ef1 Initial import: WebAisMap
Closes TG-4

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-04 07:56:45 +03:00

701 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
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,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>