From 8b177097a302c4cc63796e15fb369e3b5582d7eb Mon Sep 17 00:00:00 2001 From: dv Date: Tue, 28 Apr 2026 16:12:25 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=BA=D0=B0=D1=81?= =?UTF-8?q?=D1=81=D1=8B=20+=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D1=82=D0=BE=D0=B9=D0=BA=D0=B5=20=D0=B2=20?= =?UTF-8?q?=D0=BC=D0=BD=D0=B5=D0=BC=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 15 ++++++ CLAUDE.md | 98 +++++++++++++++++++++++++++++++++++++++ js/options.js | 9 ++++ js/strings.js | 7 +++ manifest.json | 2 +- modules/core.js | 19 ++++++-- modules/novnc-redirect.js | 19 ++++++-- modules/video-window.js | 50 ++++++++++++++++---- options.html | 15 ++++++ 9 files changed, 218 insertions(+), 16 deletions(-) create mode 100644 CLAUDE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f86be3a..bd050de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ --- +## [2.9] — 2026-04-28 + +### Добавлено + +- **Режим отображения IP-адреса в видео-окне** — новая настройка в разделе «Видео окна»: выбор между двумя вариантами: + - **Кнопка с IP-адресом** (по умолчанию) — кнопка `🔗 IP:порт` добавляется рядом с заголовком окна + - **Кнопка «Лещ»** — кнопка с фиксированным текстом «Лещ» и tooltip-подсказкой с URL устройства +- **Поддержка нового типа noVNC-страницы** — страница «Платёжный терминал» теперь корректно определяется как noVNC по `` (работает до инициализации React) + +### Исправлено + +- **Переадресация кассы** — пофиксил редирект на кассе, оттуда тоже перекидывает на порт 5000 + +--- + ## [2.8] — 2026-03-23 ### Добавлено diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..616fe4b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,98 @@ +# CAPS Enhancer — Chrome Extension + +Chrome-расширение (Manifest V3) для улучшения интерфейса CAPS/UTP: навигационные кнопки, автологин, замена логотипа, редирект noVNC. + +## Архитектура + +``` +caps_enhancer-1/ +├── manifest.json # MV3, update URL, content scripts on <all_urls> +├── js/ +│ ├── background.js # Service worker: проверка обновлений (chrome.alarms) +│ ├── content.js # Инициализация: роутинг по типу страницы +│ ├── popup.js # Popup: быстрые переключатели +│ ├── options.js # Страница настроек: загрузка/сохранение конфига +│ └── strings.js # Все строки UI (русский) +├── modules/ +│ ├── core.js # CONFIG, утилиты, loadConfig(), updateConfig() +│ ├── mnemoschema.js # Кнопки «Мнемосхема» и «Лещ» на главной странице +│ ├── video-window.js # Кнопка с IP или «Лещ» в заголовке видео-окна +│ ├── novnc-redirect.js # Редирект или кнопка на noVNC-странице +│ ├── auto-login.js # Автологин с профилями и правилами подсетей +│ └── close-handler.js # ESC закрывает видео-окна и карточки +├── options.html / popup.html +├── css/options.css / popup.css +└── server/ # Деплой: .crx, nginx.conf, .reg, update.xml +``` + +## Конфигурация (core.js CONFIG) + +Хранится в `chrome.storage.sync` под ключом `config`. Структура: + +```javascript +{ + debug: { enabled, modules: { core, video, mnemo, close, content, novnc, autologin } }, + ports: { video: '5000', scheme: '8080' }, + videoWindow: { + ipButtonMode: 'button' // 'button' — кнопка «🔗 IP:порт», 'clickable-title' — кнопка «Лещ» + }, + novnc: { mode: 'button'|'redirect', redirectDelay: 500 }, + mnemo: { mode: 'host-port'|'host-scheme', skipOnDomain, addVideoButton, addSchemeButton }, + autologin: { enabled, profilePage1, profilePage2, subnetRules, profiles }, + disabledSites: [], + replaceLogo: true, + uiVisible: true +} +``` + +При добавлении нового поля конфига: обновить `CONFIG` в `core.js`, `updateConfig()` в `core.js`, `DEFAULT_CONFIG` в `options.js`, добавить элемент в `options.html`, загрузку/сохранение в `options.js`, строки в `strings.js`. + +## Типы страниц (core.checkPageType) + +| Тип | Признаки | +|-----|---------| +| `isMainPage` | логотип CleverPark + noscript-маркер | +| `isVideoPage` | noscript-маркер, нет логотипа | +| `isNoVNCPage` | noscript "You need to enable JavaScript" + один из признаков ниже | + +**Признаки noVNC (второй критерий):** +- `div[class*="app_AppWindow__"]` — старый тип страницы (CSS-модульный класс React-компонента) +- `document.title` содержит `"Платёжный терминал"` — новый тип страницы (присутствует в HTML сразу при загрузке, до инициализации React) + +**Важно:** не использовать DOM-элементы, которые появляются после загрузки React (например `div#app.App`, `div.multi-find-menu-container`) — они не существуют в момент первой проверки, что приводит к неверному определению типа страницы. + +## Модуль video-window.js + +Добавляет ссылку на IP устройства в заголовок видео-окна. Два режима (настройка `videoWindow.ipButtonMode`): + +- **`button`** (по умолчанию): кнопка `🔗 IP:5000` после `<p>` в заголовке. Tooltip не задан. +- **`clickable-title`**: кнопка **«Лещ»** с тем же стилем и тем же переходом. При наведении — нативный tooltip с полным URL (`button.title`). + +Оба режима используют `core.createStyledButton()` — одинаковая стилизация. IP извлекается из src видео-элемента `img[src*=":8888/screen"][src*="/getVideo/"]` через regex `/\/getVideo\/http:\/\/([^\/:?#]+)(?::(\d+))?/i`. + +## Модуль novnc-redirect.js + +Два режима (`novnc.mode`): + +- **`redirect`**: автоматический переход на `http://<host>:<videoPort>`. Редирект происходит если текущий порт **не равен** целевому (не жёсткая привязка к порту 80). +- **`button`**: фиксированная кнопка в правом верхнем углу. После добавления следит за удалением через `MutationObserver` (15 сек) и добавляет кнопку снова — защита от React-рендеринга, который может очищать body. + +Важно: у кнопки задан `dataset.capsUiDisplay = 'inline-block'`, чтобы `applyVisibility` не сбрасывала её display в `''`. + +## Видимость UI (Shift+F1) + +- `data-caps-ui="true"` — элементы расширения (скрываются при `uiVisible=false`) +- `data-caps-ui-display` — исходное значение `display`, восстанавливается при `uiVisible=true`; **обязательно задавать** при создании кнопок с нестандартным display +- `data-caps-original="true"` — оригинальные элементы (скрываются при `uiVisible=true`) +- Логотип меняет `src` вместо `display` + +## Паттерны + +- Каждый модуль — IIFE, расширяет `window.VideoIPRedirect` +- Общие утилиты: `core.createStyledButton()`, `core.debugLog()`, `core.retryWithBackoff()` +- Конфиг обновляется через `chrome.runtime.onMessage` → `CONFIG_UPDATED` +- Строки: все тексты UI только в `js/strings.js`, доступ через `getStr('options.key')` + +## Деплой + +См. [DEPLOY.md](DEPLOY.md). Кратко: упаковать `.crx` → загрузить на сервер → обновить `update.xml` с новой версией → версия в `manifest.json`. diff --git a/js/options.js b/js/options.js index d2fcbd1..c883aed 100644 --- a/js/options.js +++ b/js/options.js @@ -50,6 +50,7 @@ const DEFAULT_CONFIG = { { name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 } ] }, + videoWindow: { ipButtonMode: 'button' }, disabledSites: [], replaceLogo: true, uiVisible: true @@ -70,6 +71,7 @@ const elements = { mnemoSkipOnDomain: document.getElementById('mnemo-skip-on-domain'), mnemoAddSchemeButton: document.getElementById('mnemo-add-scheme-button'), mnemoAddVideoButton: document.getElementById('mnemo-add-video-button'), + videoIpButtonMode: document.getElementById('video-ip-button-mode'), replaceLogo: document.getElementById('replace-logo'), uiVisible: document.getElementById('ui-visible'), autologinEnabled: document.getElementById('autologin-enabled'), @@ -350,6 +352,9 @@ function loadSettings() { elements.portVideo.value = config.ports?.video || DEFAULT_CONFIG.ports.video; elements.portScheme.value = config.ports?.scheme || DEFAULT_CONFIG.ports.scheme; + // Видео окна + elements.videoIpButtonMode.value = config.videoWindow?.ipButtonMode || DEFAULT_CONFIG.videoWindow.ipButtonMode; + // noVNC elements.novncRedirect.checked = (config.novnc?.mode || DEFAULT_CONFIG.novnc.mode) === 'redirect'; elements.redirectDelay.value = config.novnc?.redirectDelay || DEFAULT_CONFIG.novnc.redirectDelay; @@ -440,6 +445,9 @@ function saveSettings() { } }, ports: { video: portVideo, scheme: portScheme }, + videoWindow: { + ipButtonMode: elements.videoIpButtonMode.value + }, novnc: { mode: elements.novncRedirect.checked ? 'redirect' : 'button', redirectDelay: parseInt(elements.redirectDelay.value) || 500 @@ -493,6 +501,7 @@ function resetSettings() { elements.portVideo.value = DEFAULT_CONFIG.ports.video; elements.portScheme.value = DEFAULT_CONFIG.ports.scheme; + elements.videoIpButtonMode.value = DEFAULT_CONFIG.videoWindow.ipButtonMode; elements.novncRedirect.checked = DEFAULT_CONFIG.novnc.mode === 'redirect'; elements.redirectDelay.value = DEFAULT_CONFIG.novnc.redirectDelay; elements.mnemoMode.value = DEFAULT_CONFIG.mnemo.mode; diff --git a/js/strings.js b/js/strings.js index 390f08d..3718dad 100644 --- a/js/strings.js +++ b/js/strings.js @@ -29,6 +29,13 @@ window.CAPS_STRINGS = { headerTitle: "Настройки CAPS Enhancer", headerSubtitle: "Настройте расширение под свои нужды", + // Видео окна + videoWindowTitle: "🎬 Видео окна", + videoIpButtonModeLabel: "Режим отображения IP-адреса", + videoIpButtonModeDesc: "Как отображать ссылку на настройки устройства в заголовке видео-окна", + videoIpButtonModeButton: "Кнопка с IP-адресом (🔗 IP:порт)", + videoIpButtonModeClickable: "Кнопка «Лещ» (без IP-адреса)", + // noVNC (страница экрана) novncTitle: "🖥️ Страница экрана", novncRedirectLabel: "Включить переадресацию", diff --git a/manifest.json b/manifest.json index 7b9cf22..93a7b66 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "CAPS Enhancer", - "version": "2.8", + "version": "2.9", "description": "Добавляет всякие фичи для УТП в CAPS", "update_url": "https://git.ihateamerica.ru/caps_enhancer/update.xml", "icons": { diff --git a/modules/core.js b/modules/core.js index b1f545f..dbae42e 100644 --- a/modules/core.js +++ b/modules/core.js @@ -54,6 +54,11 @@ let CONFIG = { // Замена логотипа CleverPark на иконку CAPS (icons/caps.svg) replaceLogo: true, + // Настройки видео-окна + videoWindow: { + ipButtonMode: 'button' // 'button' — отдельная кнопка с IP, 'clickable-title' — заголовок окна становится кликабельным + }, + // Показывать ли улучшения расширения (кнопки, логотип) uiVisible: true }; @@ -123,6 +128,11 @@ function updateConfig(newConfig) { CONFIG.replaceLogo = newConfig.replaceLogo; } + // Обновляем videoWindow + if (newConfig.videoWindow) { + Object.assign(CONFIG.videoWindow, newConfig.videoWindow); + } + // Обновляем uiVisible if (typeof newConfig.uiVisible === 'boolean') { CONFIG.uiVisible = newConfig.uiVisible; @@ -252,9 +262,12 @@ function checkNoVNCPage() { } // Признак 2: div с классом начинающимся на "app_AppWindow__" - const appWindow = document.querySelector('div[class*="app_AppWindow__"]'); - const hasAppWindow = !!appWindow; - + // ИЛИ title страницы содержит "Платёжный терминал" (присутствует сразу при загрузке) + const hasAppWindow = !!( + document.querySelector('div[class*="app_AppWindow__"]') || + document.title.includes('Платёжный терминал') + ); + // Страница noVNC определяется по наличию обоих признаков return hasNoVNCNoscript && hasAppWindow; } diff --git a/modules/novnc-redirect.js b/modules/novnc-redirect.js index e372471..ca8c065 100644 --- a/modules/novnc-redirect.js +++ b/modules/novnc-redirect.js @@ -52,9 +52,9 @@ debugLog('Текущий хост:', currentHost); debugLog('Текущий порт:', currentPort); - // Проверяем, что мы на 80 порту - if (currentPort !== '80' && currentPort !== '') { - debugLog('❌ Не на 80 порту, переадресация не нужна'); + // Проверяем, что мы не уже на целевом порту + if (currentPort === core.TARGET_PORT_VIDEO) { + debugLog('❌ Уже на целевом порту, переадресация не нужна'); return false; } @@ -105,6 +105,7 @@ ); button.setAttribute('data-caps-ui', 'true'); + button.dataset.capsUiDisplay = 'inline-block'; // Дополнительные стили для позиционирования button.style.position = 'fixed'; @@ -117,8 +118,18 @@ // Добавляем кнопку на страницу document.body.appendChild(button); - debugLog('✅ Кнопка настроек добавлена'); + + // React-приложение может заменить содержимое body — следим за удалением кнопки + const observer = new MutationObserver(() => { + if (!document.querySelector('.novnc-settings-btn')) { + debugLog('⚠️ Кнопка удалена, добавляем снова'); + document.body.appendChild(button); + } + }); + observer.observe(document.body, { childList: true, subtree: false }); + setTimeout(() => observer.disconnect(), 15000); + return true; } diff --git a/modules/video-window.js b/modules/video-window.js index 0f74ab8..79acbd8 100644 --- a/modules/video-window.js +++ b/modules/video-window.js @@ -66,12 +66,12 @@ return null; } - // Функция для добавления кнопки в заголовок + // Режим А: добавить отдельную кнопку рядом с заголовком function addButtonToHeader(header, ip) { if (header.querySelector('.video-redirect-btn')) { return null; } - + const button = core.createStyledButton( `🔗 ${ip}:${core.TARGET_PORT_VIDEO}`, (e) => { @@ -81,7 +81,7 @@ 'video-redirect-btn' ); button.setAttribute('data-caps-ui', 'true'); - + const p = header.querySelector('p'); if (p) { p.style.display = 'inline-block'; @@ -90,7 +90,36 @@ } else { header.appendChild(button); } - + + return button; + } + + // Режим Б: кнопка «Лещ» вместо IP-адреса + function addLeshButtonToHeader(header, ip) { + if (header.querySelector('.video-redirect-btn')) { + return null; + } + + const button = core.createStyledButton( + 'Лещ', + (e) => { + e.stopPropagation(); + window.open(`http://${ip}:${core.TARGET_PORT_VIDEO}`, '_blank'); + }, + 'video-redirect-btn' + ); + button.setAttribute('data-caps-ui', 'true'); + button.title = `http://${ip}:${core.TARGET_PORT_VIDEO}`; + + const p = header.querySelector('p'); + if (p) { + p.style.display = 'inline-block'; + p.style.marginRight = '8px'; + p.insertAdjacentElement('afterend', button); + } else { + header.appendChild(button); + } + return button; } @@ -116,8 +145,14 @@ return false; } - addButtonToHeader(activeWindow.header, ipData.ip); - debugLog('✅ Кнопка добавлена'); + const mode = core.CONFIG.videoWindow?.ipButtonMode || 'button'; + if (mode === 'clickable-title') { + addLeshButtonToHeader(activeWindow.header, ipData.ip); + debugLog('✅ Кнопка «Лещ» добавлена'); + } else { + addButtonToHeader(activeWindow.header, ipData.ip); + debugLog('✅ Кнопка добавлена'); + } debugLog('========== КОНЕЦ =========='); return true; @@ -127,8 +162,7 @@ window.VideoIPRedirect.VideoWindow = { addButtonToVideoWindow: addButtonToActiveWindow, removeButtonsFromWindow: function() { - const btns = document.querySelectorAll('.video-redirect-btn'); - btns.forEach(btn => btn.remove()); + document.querySelectorAll('.video-redirect-btn').forEach(btn => btn.remove()); } }; diff --git a/options.html b/options.html index c1756f9..a895f5c 100644 --- a/options.html +++ b/options.html @@ -21,6 +21,21 @@ <div class="content"> <div class="settings-grid"> <div class="settings-col"> + <!-- Настройки видео-окна --> + <section class="settings-section"> + <h2 data-i18n="options.videoWindowTitle">🎬 Видео окна</h2> + <div class="setting-item"> + <label for="video-ip-button-mode"> + <span class="label-text" data-i18n="options.videoIpButtonModeLabel">Режим кнопки с IP-адресом</span> + <span class="label-description" data-i18n="options.videoIpButtonModeDesc">Как отображать ссылку на настройки устройства в заголовке видео-окна</span> + </label> + <select id="video-ip-button-mode"> + <option value="button" data-i18n="options.videoIpButtonModeButton">Кнопка с IP-адресом (🔗 IP:порт)</option> + <option value="clickable-title" data-i18n="options.videoIpButtonModeClickable">Кнопка «Лещ» (без IP-адреса)</option> + </select> + </div> + </section> + <!-- Настройки noVNC (страница экрана) --> <section class="settings-section"> <h2 data-i18n="options.novncTitle">🖥️ Страница экрана</h2>