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
+├── 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` после `
` в заголовке. 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://:`. Редирект происходит если текущий порт **не равен** целевому (не жёсткая привязка к порту 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 @@