diff --git a/README.md b/README.md index bc79e27..b4ae896 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The project serves as a real-time monitoring and diagnostic tool for the Meshtas - **IMPORTANT:** the predicted coverage feature requires the extra `pyitm` dependency. If it is not installed, the coverage API will return 503. - Ubuntu install (inside the venv): `./env/bin/pip install pyitm` - Coverage: predicted coverage overlay (Longley‑Rice area mode) with perimeter rendering and documentation. +- UI: added QR code display for quick node/app access. - Gateways: persistent gateway tracking (`is_mqtt_gateway`) and UI indicators in nodes, map popups, and stats. - Map UX: deterministic jitter for overlapping nodes; edges follow jittered positions. - Tooling: Meshtastic protobuf updater script with `--check` and `UPSTREAM_REV.txt` tracking. diff --git a/meshview/lang/ru.json b/meshview/lang/ru.json new file mode 100644 index 0000000..f914d59 --- /dev/null +++ b/meshview/lang/ru.json @@ -0,0 +1,252 @@ +{ + "base": { + "chat": "Чат", + "nodes": "Ноды", + "everything": "Показать все", + "graphs": "Mesh графики", + "net": "Недельная сеть", + "map": "Живая карта", + "stats": "Статистика", + "top": "Крупнейшие ноды трафика", + "footer": "Посетите Meshview на GitHub", + "node id": "ID ноды", + "go to node": "Перейти к ноде", + "all": "Все", + "portnum_options": { + "0": "Неизвестно", + "1": "Текстовое сообщение", + "2": "Удалённое оборудование", + "3": "Позиция", + "4": "Информация о ноде", + "5": "Маршрутизация", + "6": "Администрирование", + "7": "Текст (сжатый)", + "8": "Контрольная точка", + "9": "Аудио", + "10": "Датчик обнаружения", + "11": "Тревога", + "12": "Проверка ключа", + "32": "Ответить", + "33": "IP туннель", + "34": "Счетчик прохожих", + "35": "Store Forward++", + "36": "Состояние ноды", + "64": "Последовательный", + "65": "Store & Forward", + "66": "Проверка дальности", + "67": "Телеметрия", + "68": "ZPS", + "69": "Симулятор", + "70": "Трассировка", + "71": "Neighbor Info", + "72": "ATAK", + "73": "Отчёт по карте", + "74": "Power Stress", + "76": "Reticulum туннель", + "77": "Cayenne", + "256": "Частное приложение", + "257": "Пересылка ATAK" + } + }, + "chat": { + "chat_title": "Чаты:", + "replying_to": "В ответ на:", + "view_packet_details": "Просмотреть детали пакета" + }, + "nodelist": { + "search_placeholder": "Поиск по имени или ID...", + "all_roles": "Все роли", + "all_channels": "Все каналы", + "all_hw": "Все модели устройств", + "all_firmware": "Все прошивки", + "show_favorites": "⭐ Показать избранные", + "show_all": "⭐ Показать все", + "export_csv": "Экспорт CSV", + "clear_filters": "Очистить фильтры", + "showing_nodes": "Показать ноды", + "nodes_suffix": "ноды", + "loading_nodes": "Загрузка нод...", + "error_loading_nodes": "Ошибка загрузки нод", + "no_nodes_found": "Ноды не найдены", + "short_name": "Короткое имя", + "long_name": "Длинное имя", + "hw_model": "Модель оборудования", + "firmware": "Прошивка", + "role": "Роль", + "last_lat": "Последняя широта", + "last_long": "Последняя долгота", + "channel": "Канал", + "mqtt_gateway": "MQTT", + "last_seen": "Был в сети", + "favorite": "Избранные", + "yes": "Да", + "no": "Нет", + "time_just_now": "только что", + "time_min_ago": "минут назад", + "time_hr_ago": "часов назад", + "time_day_ago": "день назад", + "time_days_ago": "дней назад" + }, + "net": { + "net_title": "Еженедельная сеть:", + "total_messages": "Количество сообщений:", + "view_packet_details": "Больше деталей" + }, + "map": { + "show_routers_only": "Показать только маршрутизаторы", + "show_mqtt_only": "Показать только шлюзы MQTT", + "share_view": "Поделиться этим видом", + "reset_filters": "Сбросить фильтры", + "unmapped_packets_title": "Несопоставленные пакеты", + "unmapped_packets_empty": "Нет последних несопоставленных пакетов.", + "channel_label": "Канал:", + "model_label": "Модель:", + "role_label": "Роль:", + "mqtt_gateway": "Шлюз MQTT:", + "last_seen": "Последний раз слышен:", + "firmware": "Прошивка:", + "yes": "Да", + "no": "Нет", + "link_copied": "Ссылка скопирована!", + "legend_traceroute": "Трассировка (со стрелками)", + "legend_neighbor": "Сосед" + }, + "stats": { + "mesh_stats_summary": "Сводка статистики сети (все доступные в базе данных)", + "total_nodes": "Всего нод", + "total_gateways": "Всего шлюзов", + "total_packets": "Всего пакетов", + "total_packets_seen": "Всего пакетов обнаружено", + "packets_per_day_all": "Пакетов в день - все порты (за последние 14 дней)", + "packets_per_day_text": "Пакетов в день - текстовые сообщения (порт 1 за последние 14 дней)", + "packets_per_hour_all": "Пакетов в час - все порты", + "packets_per_hour_text": "Пакетов в час - текстовые сообщения (порт 1)", + "packet_types_last_24h": "Типы пакетов - последние 24 часа", + "hardware_breakdown": "Распределение устройств", + "role_breakdown": "Распределение ролей", + "channel_breakdown": "Распределение каналов", + "gateway_channel_breakdown": "Распределение шлюзов канала", + "gateway_role_breakdown": "Распределение ролей шлюза", + "gateway_firmware_breakdown": "Распределение прошивки шлюза", + "no_gateways": "Шлюзов не найдено", + "expand_chart": "Развернуть диаграмму", + "export_csv": "Экспорт CSV", + "all_channels": "Все каналы", + "node_id": "ID ноды" + }, + "top": { + "top_traffic_nodes": " Трафик нод", + "channel": "Канал", + "search": "Поиск", + "search_placeholder": "Поиск нод...", + "long_name": "Длинное имя", + "short_name": "Короткое имя", + "packets_sent": "Отправлено (24ч)", + "times_seen": "Принято (24ч)", + "avg_gateways": "Среднее количество шлюзов", + "showing_nodes": "Показать", + "nodes_suffix": "ноды" + }, + "nodegraph": { + "channel_label": "Канал:", + "search_node_placeholder": "Поиск ноды...", + "search_button": "Поиск", + "long_name_label": "Полное имя:", + "short_name_label": "Короткое имя:", + "role_label": "Роль:", + "hw_model_label": "Модель оборудования:", + "node_not_found": "Узел не найден в текущем канале!" + }, + "firehose": { + "live_feed": "📡 Прямой эфир", + "pause": "Пауза", + "resume": "Продолжить", + "time": "Время", + "packet_id": "ID пакета", + "from": "От", + "to": "К", + "port": "Порт", + "links": "Ссылки", + "unknown_app": "НЕИЗВ-НОЕ ПРИЛОЖ", + "text_message": "Текстовое сообщение", + "position": "Позиция", + "node_info": "Информация об узле", + "routing": "Маршрутизация", + "administration": "Администрирование", + "waypoint": "Контрольная точка", + "store_forward": "Store Forward", + "telemetry": "Телеметрия", + "trace_route": "Трассировка маршрута", + "neighbor_info": "Neighbor Info", + "direct_to_mqtt": "напрямую к MQTT", + "all": "Все", + "map": "Карта", + "graph": "Диаграмма" + }, + "node": { + "specifications": "Технические характеристики", + "node_id": "ID ноды", + "long_name": "Длинное имя", + "short_name": "Короткое имя", + "hw_model": "Модель оборудования", + "firmware": "Прошивка", + "role": "Роль", + "mqtt_gateway": "Шлюз MQTT", + "channel": "Канал", + "latitude": "Широта", + "longitude": "Долгота", + "first_update": "Первое обновление", + "last_update": "Последнее обновление", + "battery_voltage": "Батарея и напряжение", + "air_channel": "Использование сети и каналов", + "environment": "Метрики среды", + "neighbors_chart": "Соседи (Соотношение сигнал/шум)", + "expand": "Расширить", + "export_csv": "Экспорт в CSV", + "time": "Время", + "packet_id": "ID пакета", + "from": "От", + "to": "К", + "port": "Порт", + "direct_to_mqtt": "Напрямую к MQTT", + "all_broadcast": "Все", + "statistics": "Статистика", + "last_24h": "24ч", + "packets_sent": "Отправленные пакеты", + "times_seen": "Просмотрено раз", + "yes": "Да", + "no": "Нет", + "copy_import_url": "Копировать URL для импорта", + "show_qr_code": "Показать QR-код", + "toggle_coverage": "Прогнозируемое покрытие", + "location_required": "Требуется местоположение для покрытия", + "coverage_help": "Помощь по покрытию", + "share_contact_qr": "Поделиться QR контакта", + "copy_url": "Копировать URL", + "copied": "Скопировано!", + "potential_impersonation": "Обнаружено возможное самозванство", + "scan_qr_to_add": "Отсканируйте этот QR-код, чтобы добавить эту ноду в качестве контакта на другом устройстве." + }, + "packet": { + "loading": "Загрузка информации о пакете...", + "packet_id_label": "ID пакета", + "from_node": "От ноды", + "to_node": "К ноде", + "channel": "Канал", + "port": "Порт", + "raw_payload": "Незашифрованная нагрузка", + "decoded_telemetry": "Раскодированная телеметрия", + "location": "Местоположение", + "seen_by": "Просмотрено", + "gateway": "Шлюз", + "rssi": "RSSI", + "snr": "SNR", + "hops": "Хоп", + "time": "Время", + "packet_source": "Источник пакета", + "distance": "Расстояние", + "node_id_short": "ID ноды", + "all_broadcast": "Все", + "direct_to_mqtt": "Напрямую к MQTT" + } +} diff --git a/meshview/templates/map.html b/meshview/templates/map.html index 6a21415..bdc1df8 100644 --- a/meshview/templates/map.html +++ b/meshview/templates/map.html @@ -228,6 +228,44 @@ function hashToColor(str){ return c; } +function labelFor(key, fallback){ + return mapTranslations[key] || fallback; +} + +function buildNodePopup(node){ + const labels = { + channel: labelFor("channel_label", "Channel"), + model: labelFor("model_label", "Model"), + role: labelFor("role_label", "Role"), + mqtt: labelFor("mqtt_gateway", "MQTT Gateway"), + lastSeen: labelFor("last_seen", "Last Seen"), + firmware: labelFor("firmware", "Firmware"), + yes: mapTranslations.yes || "Yes", + no: mapTranslations.no || "No" + }; + + return ` + ${node.long_name} (${node.short_name})
+ + ${labels.channel} ${node.channel}
+ ${labels.model} ${node.hw_model}
+ ${labels.role} ${node.role}
+ ${labels.mqtt} ${node.is_mqtt_gateway ? labels.yes : labels.no}
+ + ${ + node.last_seen_us + ? `${labels.lastSeen} ${timeAgoFromUs(node.last_seen_us)}
` + : "" + } + + ${ + node.firmware + ? `${labels.firmware} ${node.firmware}
` + : "" + } + `; +} + function hashToUnit(str){ let h = 2166136261; for(let i=0;i { onNodeClick(node); - marker.bindPopup(popup).openPopup(); + marker.setPopupContent(buildNodePopup(node)); + marker.openPopup(); }); }); diff --git a/meshview/templates/node.html b/meshview/templates/node.html index d3c8a7c..50d2ba5 100644 --- a/meshview/templates/node.html +++ b/meshview/templates/node.html @@ -898,13 +898,21 @@ function addMarker(id, lat, lon, color = "red", node = null) { async function drawNeighbors(src, nids) { if (!map) return; - // Ensure source node position exists - const srcNode = await fetchNodeFromApi(src); - if (!srcNode || !srcNode.last_lat || !srcNode.last_long) return; + // Prefer the currently displayed source position (e.g. latest track point), + // then fall back to the node API location. + let srcLat; + let srcLon; + let srcNode = currentNode || nodeCache[src] || null; - const srcLat = srcNode.last_lat / 1e7; - const srcLon = srcNode.last_long / 1e7; - nodePositions[src] = [srcLat, srcLon]; + if (nodePositions[src]) { + [srcLat, srcLon] = nodePositions[src]; + } else { + srcNode = srcNode || await fetchNodeFromApi(src); + if (!srcNode || !srcNode.last_lat || !srcNode.last_long) return; + srcLat = srcNode.last_lat / 1e7; + srcLon = srcNode.last_long / 1e7; + nodePositions[src] = [srcLat, srcLon]; + } for (const nid of nids) { const neighbor = await fetchNodeFromApi(nid); @@ -1627,15 +1635,16 @@ document.addEventListener("DOMContentLoaded", async () => { // ✅ MAP MUST EXIST FIRST if (!map) initMap(); + // Load the track first so neighbor links anchor to the same + // visible current-node position shown on this page. + await loadTrack(); + // ✅ DRAW LATEST NEIGHBORS ONCE const neighborIds = await loadLatestNeighborIds(); if (neighborIds.length) { await drawNeighbors(fromNodeId, neighborIds); } - // ⚠️ Track may add to map, but must not hide it - await loadTrack(); - await loadPackets(); initPacketPortFilter(); await loadTelemetryCharts(); diff --git a/meshview/templates/nodelist.html b/meshview/templates/nodelist.html index 58aa0d6..d0c92ed 100644 --- a/meshview/templates/nodelist.html +++ b/meshview/templates/nodelist.html @@ -266,7 +266,7 @@ select, .export-btn, .search-box, .clear-btn { Last Latitude Last Longitude Channel - MQTT + MQTT Last Seen @@ -331,7 +331,7 @@ const minStatusMs = 300; const headers = document.querySelectorAll("thead th"); const keyMap = [ "short_name","long_name","hw_model","firmware","role", - "last_lat","last_long","channel","last_seen_us" + "last_lat","last_long","channel","is_mqtt_gateway","last_seen_us" ]; function debounce(fn, delay = 250) { @@ -717,6 +717,11 @@ document.addEventListener("DOMContentLoaded", async function() { B = B || 0; } + if (key === "is_mqtt_gateway") { + A = A ? 1 : 0; + B = B ? 1 : 0; + } + // Normalize strings for stable sorting if (typeof A === "string") A = A.toLowerCase(); if (typeof B === "string") B = B.toLowerCase(); diff --git a/meshview/templates/packet.html b/meshview/templates/packet.html index 42e3122..0be014a 100644 --- a/meshview/templates/packet.html +++ b/meshview/templates/packet.html @@ -408,6 +408,11 @@ seenSorted.forEach(s => { hopGroups[hopValue].push(s); }); +function formatHopDisplay(hopKey, hopStart){ + const startVal = hopStart ?? "—"; + return `${hopKey}/${startVal}`; +} + /* --------------------------------------------- Render grouped gateway table + map markers ----------------------------------------------*/ @@ -424,6 +429,7 @@ seenTableBody.innerHTML = Object.keys(hopGroups) const node = nodeLookup[s.node_id]; const label = node?.long_name || s.node_id; + const hopDisplay = formatHopDisplay(hopKey, s.hop_start); const timeStr = s.import_time_us ? new Date(s.import_time_us/1000).toLocaleTimeString() : "—"; @@ -482,7 +488,7 @@ seenTableBody.innerHTML = Object.keys(hopGroups) RSSI: ${s.rx_rssi ?? "—"}
SNR: ${s.rx_snr ?? "—"}

- Hops: ${hopKey} + Hops: ${hopDisplay} `); @@ -493,7 +499,7 @@ seenTableBody.innerHTML = Object.keys(hopGroups) ${label} ${s.rx_rssi ?? "—"} ${s.rx_snr ?? "—"} - ${hopKey} + ${hopDisplay} ${s.channel ?? "—"} ${timeStr}