фикс кассы + кнопки на стойке в мнеме

This commit is contained in:
dv 2026-04-28 16:12:25 +03:00
parent 36998bbcce
commit 8b177097a3
9 changed files with 218 additions and 16 deletions

View file

@ -2,6 +2,21 @@
--- ---
## [2.9] — 2026-04-28
### Добавлено
- **Режим отображения IP-адреса в видео-окне** — новая настройка в разделе «Видео окна»: выбор между двумя вариантами:
- **Кнопка с IP-адресом** (по умолчанию) — кнопка `🔗 IP:порт` добавляется рядом с заголовком окна
- **Кнопка «Лещ»** — кнопка с фиксированным текстом «Лещ» и tooltip-подсказкой с URL устройства
- **Поддержка нового типа noVNC-страницы** — страница «Платёжный терминал» теперь корректно определяется как noVNC по `<title>` (работает до инициализации React)
### Исправлено
- **Переадресация кассы** — пофиксил редирект на кассе, оттуда тоже перекидывает на порт 5000
---
## [2.8] — 2026-03-23 ## [2.8] — 2026-03-23
### Добавлено ### Добавлено

98
CLAUDE.md Normal file
View file

@ -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`.

View file

@ -50,6 +50,7 @@ const DEFAULT_CONFIG = {
{ name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 } { name: 'Профиль 1', username: '', password: '', autoSubmit: true, delay: 500 }
] ]
}, },
videoWindow: { ipButtonMode: 'button' },
disabledSites: [], disabledSites: [],
replaceLogo: true, replaceLogo: true,
uiVisible: true uiVisible: true
@ -70,6 +71,7 @@ const elements = {
mnemoSkipOnDomain: document.getElementById('mnemo-skip-on-domain'), mnemoSkipOnDomain: document.getElementById('mnemo-skip-on-domain'),
mnemoAddSchemeButton: document.getElementById('mnemo-add-scheme-button'), mnemoAddSchemeButton: document.getElementById('mnemo-add-scheme-button'),
mnemoAddVideoButton: document.getElementById('mnemo-add-video-button'), mnemoAddVideoButton: document.getElementById('mnemo-add-video-button'),
videoIpButtonMode: document.getElementById('video-ip-button-mode'),
replaceLogo: document.getElementById('replace-logo'), replaceLogo: document.getElementById('replace-logo'),
uiVisible: document.getElementById('ui-visible'), uiVisible: document.getElementById('ui-visible'),
autologinEnabled: document.getElementById('autologin-enabled'), autologinEnabled: document.getElementById('autologin-enabled'),
@ -350,6 +352,9 @@ function loadSettings() {
elements.portVideo.value = config.ports?.video || DEFAULT_CONFIG.ports.video; elements.portVideo.value = config.ports?.video || DEFAULT_CONFIG.ports.video;
elements.portScheme.value = config.ports?.scheme || DEFAULT_CONFIG.ports.scheme; elements.portScheme.value = config.ports?.scheme || DEFAULT_CONFIG.ports.scheme;
// Видео окна
elements.videoIpButtonMode.value = config.videoWindow?.ipButtonMode || DEFAULT_CONFIG.videoWindow.ipButtonMode;
// noVNC // noVNC
elements.novncRedirect.checked = (config.novnc?.mode || DEFAULT_CONFIG.novnc.mode) === 'redirect'; elements.novncRedirect.checked = (config.novnc?.mode || DEFAULT_CONFIG.novnc.mode) === 'redirect';
elements.redirectDelay.value = config.novnc?.redirectDelay || DEFAULT_CONFIG.novnc.redirectDelay; elements.redirectDelay.value = config.novnc?.redirectDelay || DEFAULT_CONFIG.novnc.redirectDelay;
@ -440,6 +445,9 @@ function saveSettings() {
} }
}, },
ports: { video: portVideo, scheme: portScheme }, ports: { video: portVideo, scheme: portScheme },
videoWindow: {
ipButtonMode: elements.videoIpButtonMode.value
},
novnc: { novnc: {
mode: elements.novncRedirect.checked ? 'redirect' : 'button', mode: elements.novncRedirect.checked ? 'redirect' : 'button',
redirectDelay: parseInt(elements.redirectDelay.value) || 500 redirectDelay: parseInt(elements.redirectDelay.value) || 500
@ -493,6 +501,7 @@ function resetSettings() {
elements.portVideo.value = DEFAULT_CONFIG.ports.video; elements.portVideo.value = DEFAULT_CONFIG.ports.video;
elements.portScheme.value = DEFAULT_CONFIG.ports.scheme; elements.portScheme.value = DEFAULT_CONFIG.ports.scheme;
elements.videoIpButtonMode.value = DEFAULT_CONFIG.videoWindow.ipButtonMode;
elements.novncRedirect.checked = DEFAULT_CONFIG.novnc.mode === 'redirect'; elements.novncRedirect.checked = DEFAULT_CONFIG.novnc.mode === 'redirect';
elements.redirectDelay.value = DEFAULT_CONFIG.novnc.redirectDelay; elements.redirectDelay.value = DEFAULT_CONFIG.novnc.redirectDelay;
elements.mnemoMode.value = DEFAULT_CONFIG.mnemo.mode; elements.mnemoMode.value = DEFAULT_CONFIG.mnemo.mode;

View file

@ -29,6 +29,13 @@ window.CAPS_STRINGS = {
headerTitle: "Настройки CAPS Enhancer", headerTitle: "Настройки CAPS Enhancer",
headerSubtitle: "Настройте расширение под свои нужды", headerSubtitle: "Настройте расширение под свои нужды",
// Видео окна
videoWindowTitle: "🎬 Видео окна",
videoIpButtonModeLabel: "Режим отображения IP-адреса",
videoIpButtonModeDesc: "Как отображать ссылку на настройки устройства в заголовке видео-окна",
videoIpButtonModeButton: "Кнопка с IP-адресом (🔗 IP:порт)",
videoIpButtonModeClickable: "Кнопка «Лещ» (без IP-адреса)",
// noVNC (страница экрана) // noVNC (страница экрана)
novncTitle: "🖥️ Страница экрана", novncTitle: "🖥️ Страница экрана",
novncRedirectLabel: "Включить переадресацию", novncRedirectLabel: "Включить переадресацию",

View file

@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "CAPS Enhancer", "name": "CAPS Enhancer",
"version": "2.8", "version": "2.9",
"description": "Добавляет всякие фичи для УТП в CAPS", "description": "Добавляет всякие фичи для УТП в CAPS",
"update_url": "https://git.ihateamerica.ru/caps_enhancer/update.xml", "update_url": "https://git.ihateamerica.ru/caps_enhancer/update.xml",
"icons": { "icons": {

View file

@ -54,6 +54,11 @@ let CONFIG = {
// Замена логотипа CleverPark на иконку CAPS (icons/caps.svg) // Замена логотипа CleverPark на иконку CAPS (icons/caps.svg)
replaceLogo: true, replaceLogo: true,
// Настройки видео-окна
videoWindow: {
ipButtonMode: 'button' // 'button' — отдельная кнопка с IP, 'clickable-title' — заголовок окна становится кликабельным
},
// Показывать ли улучшения расширения (кнопки, логотип) // Показывать ли улучшения расширения (кнопки, логотип)
uiVisible: true uiVisible: true
}; };
@ -123,6 +128,11 @@ function updateConfig(newConfig) {
CONFIG.replaceLogo = newConfig.replaceLogo; CONFIG.replaceLogo = newConfig.replaceLogo;
} }
// Обновляем videoWindow
if (newConfig.videoWindow) {
Object.assign(CONFIG.videoWindow, newConfig.videoWindow);
}
// Обновляем uiVisible // Обновляем uiVisible
if (typeof newConfig.uiVisible === 'boolean') { if (typeof newConfig.uiVisible === 'boolean') {
CONFIG.uiVisible = newConfig.uiVisible; CONFIG.uiVisible = newConfig.uiVisible;
@ -252,8 +262,11 @@ function checkNoVNCPage() {
} }
// Признак 2: div с классом начинающимся на "app_AppWindow__" // Признак 2: div с классом начинающимся на "app_AppWindow__"
const appWindow = document.querySelector('div[class*="app_AppWindow__"]'); // ИЛИ title страницы содержит "Платёжный терминал" (присутствует сразу при загрузке)
const hasAppWindow = !!appWindow; const hasAppWindow = !!(
document.querySelector('div[class*="app_AppWindow__"]') ||
document.title.includes('Платёжный терминал')
);
// Страница noVNC определяется по наличию обоих признаков // Страница noVNC определяется по наличию обоих признаков
return hasNoVNCNoscript && hasAppWindow; return hasNoVNCNoscript && hasAppWindow;

View file

@ -52,9 +52,9 @@
debugLog('Текущий хост:', currentHost); debugLog('Текущий хост:', currentHost);
debugLog('Текущий порт:', currentPort); debugLog('Текущий порт:', currentPort);
// Проверяем, что мы на 80 порту // Проверяем, что мы не уже на целевом порту
if (currentPort !== '80' && currentPort !== '') { if (currentPort === core.TARGET_PORT_VIDEO) {
debugLog('❌ Не на 80 порту, переадресация не нужна'); debugLog('❌ Уже на целевом порту, переадресация не нужна');
return false; return false;
} }
@ -105,6 +105,7 @@
); );
button.setAttribute('data-caps-ui', 'true'); button.setAttribute('data-caps-ui', 'true');
button.dataset.capsUiDisplay = 'inline-block';
// Дополнительные стили для позиционирования // Дополнительные стили для позиционирования
button.style.position = 'fixed'; button.style.position = 'fixed';
@ -117,8 +118,18 @@
// Добавляем кнопку на страницу // Добавляем кнопку на страницу
document.body.appendChild(button); document.body.appendChild(button);
debugLog('✅ Кнопка настроек добавлена'); 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; return true;
} }

View file

@ -66,7 +66,7 @@
return null; return null;
} }
// Функция для добавления кнопки в заголовок // Режим А: добавить отдельную кнопку рядом с заголовком
function addButtonToHeader(header, ip) { function addButtonToHeader(header, ip) {
if (header.querySelector('.video-redirect-btn')) { if (header.querySelector('.video-redirect-btn')) {
return null; return null;
@ -94,6 +94,35 @@
return 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;
}
// Главная функция // Главная функция
function addButtonToActiveWindow() { function addButtonToActiveWindow() {
debugLog('========== НАЧАЛО =========='); debugLog('========== НАЧАЛО ==========');
@ -116,8 +145,14 @@
return false; return false;
} }
const mode = core.CONFIG.videoWindow?.ipButtonMode || 'button';
if (mode === 'clickable-title') {
addLeshButtonToHeader(activeWindow.header, ipData.ip);
debugLog('✅ Кнопка «Лещ» добавлена');
} else {
addButtonToHeader(activeWindow.header, ipData.ip); addButtonToHeader(activeWindow.header, ipData.ip);
debugLog('✅ Кнопка добавлена'); debugLog('✅ Кнопка добавлена');
}
debugLog('========== КОНЕЦ =========='); debugLog('========== КОНЕЦ ==========');
return true; return true;
@ -127,8 +162,7 @@
window.VideoIPRedirect.VideoWindow = { window.VideoIPRedirect.VideoWindow = {
addButtonToVideoWindow: addButtonToActiveWindow, addButtonToVideoWindow: addButtonToActiveWindow,
removeButtonsFromWindow: function() { removeButtonsFromWindow: function() {
const btns = document.querySelectorAll('.video-redirect-btn'); document.querySelectorAll('.video-redirect-btn').forEach(btn => btn.remove());
btns.forEach(btn => btn.remove());
} }
}; };

View file

@ -21,6 +21,21 @@
<div class="content"> <div class="content">
<div class="settings-grid"> <div class="settings-grid">
<div class="settings-col"> <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 (страница экрана) --> <!-- Настройки noVNC (страница экрана) -->
<section class="settings-section"> <section class="settings-section">
<h2 data-i18n="options.novncTitle">🖥️ Страница экрана</h2> <h2 data-i18n="options.novncTitle">🖥️ Страница экрана</h2>