telecomkz_scraper/PROJECT_DOCUMENTATION.md
Iliyas Kyrykbayev 5cb347a44b TelecomKz Analytics Mapper: audit fixes, real event keys, 32 mapped screens
Rebuilt the capture and mapping pipeline after an audit found the simulator's
data could not be trusted:

* Hotspot coordinates never matched the screenshots. Capture now scrolls the
  page over CDP and pastes each frame at the measured scrollY, so image pixels
  and DOM coordinates share one grid by construction.
* Metrics were synthesised (1200 + n*410) and presented as analytics. Numbers
  are now attached only when the catalog has a matching row; metrics.json
  carries a `source` label and the UI says "no data" instead of showing zeros.
* Event interception hooked a connector bridge that never fires. The app posts
  to api.amplitude.com using the legacy form-urlencoded v1 API; the hook now
  reads event_type off the wire. 36 keys are verified as `observed`.
* All device access moved into tools/telecom_cdp.py: dynamic WebView socket
  discovery (the PID was hardcoded), id-matched CDP, measured native geometry.
* Editor edits can now be saved to disk; API failures no longer report success
  from a stale result file; screenId is no longer interpolated into a shell.

Screens went from 7 (with fabricated markup) to 32, all verified: image height
equals map height, no out-of-bounds hotspots, no dead links.

The id_card screenshot has been manually redacted - it showed a national ID.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 18:16:46 +05:00

552 lines
33 KiB
Markdown
Raw Permalink 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.

# 📡 TelecomKz Analytics Mapper & Visual Simulator
### Архитектура, принципы работы и результаты аудита
> Ревизия документа: 2026-08-24. Проверено на реальном устройстве
> Samsung SM-S938B (Android 16, 1080×2340, WebView Chrome 151), приложение
> `kz.telecom.app`, WebView `https://customer.telecom.kz/`.
---
## 📑 Содержание
1. [О проекте](#-1-о-проекте)
2. [Архитектура](#-2-архитектура)
3. [Система координат — главное, что нужно понять](#-3-система-координат--главное-что-нужно-понять)
4. [Достоверность данных](#-4-достоверность-данных)
5. [Функционал](#-5-функционал)
6. [Результаты аудита: что было сломано и как исправлено](#-6-результаты-аудита-что-было-сломано-и-как-исправлено)
7. [Структура проекта](#-7-структура-проекта)
8. [Запуск и типовые сценарии](#-8-запуск-и-типовые-сценарии)
---
## 🎯 1. О проекте
Визуальный инструмент для сквозного маппинга UI-элементов мобильного приложения
Казахтелеком на события Amplitude и таблицы ClickHouse: наведение на любую кнопку
показывает ключ события и продуктовые метрики, а разметка снимается прямо с
подключённого по USB смартфона.
Приложение гибридное: нативная оболочка (тулбар сверху, таб-бар снизу) плюс
Vue.js-WebView в середине. Из этого следует всё остальное — и способ съёмки, и способ
получения координат, и способ перехвата событий.
---
## 🏗️ 2. Архитектура
```mermaid
flowchart TD
subgraph Device ["📱 Android (ADB)"]
Native["Нативная оболочка: тулбар + таб-бар"]
WV["WebView (Vue, customer.telecom.kz)"]
end
subgraph Core ["⚙️ tools/telecom_cdp.py — общее ядро"]
ADB["ADB: screencap, uiautomator, input"]
Sock["Поиск сокета webview_devtools_remote_&lt;pid&gt;"]
CDP["CDP-клиент с сопоставлением id ответов"]
Geom["probe_native_layout(): реальные границы WebView и таб-бара"]
end
subgraph Tools ["🐍 Инструменты"]
Cap["capture_screen.py — сшивка по смещению прокрутки"]
Live["live_auto_recorder.py — пересборка хотспотов"]
Mon["monitor_live_events.py — перехват событий"]
Crawl["crawl_and_map_all.py — автообход"]
CH["fetch_clickhouse.py — метрики"]
end
subgraph Server ["🚀 Vite dev server"]
A1["/api/capture-adb"]
A2["/api/live-sync-webview"]
A3["/api/save-app-map"]
A4["/api/device-status"]
end
subgraph UI ["💻 React 19 + TypeScript"]
Sim["PhoneSimulator"]
Tip["HotspotTooltip"]
Ed["GuiEditor"]
end
Native --> ADB
WV --> CDP
Sock --> CDP
ADB --> Geom
Core --> Tools
Tools --> Server
Server --> UI
```
**Стек.** React 19 + TypeScript + Vite; Python 3.10+ (Pillow, OpenCV, websockets);
ADB + `uiautomator`; Chrome DevTools Protocol через `adb forward`.
Ключевое архитектурное решение аудита: весь доступ к устройству идёт через
единственный модуль `tools/telecom_cdp.py`. Раньше каждый скрипт заново реализовывал
подключение — и каждый воспроизводил одни и те же три ошибки (см. §6).
---
## 📐 3. Система координат — главное, что нужно понять
Все прямоугольники хотспотов живут в **пиксельной сетке сшитого скриншота**:
```
строки 0 .. webviewTop нативный тулбар
webviewTop .. +contentHeight полный контент WebView
последние navHeight строк нативный таб-бар
```
Элемент DOM со смещением в документе `(left, top + scrollY)` попадает в
```
x = round(left * scale)
y = webviewTop + round((top + scrollY) * scale), где scale = ширина экрана / window.innerWidth
```
На проверенном устройстве: `webviewTop = 255`, `webviewBottom = 2139`,
`scale = 1080 / 384 = 2.8125`. Но **эти числа измеряются на каждом запуске** через
`uiautomator` (границы `android.webkit.WebView` и `kz.telecom.app:id/bottomNavigation`),
а не задаются константами.
### Почему сшивка сделана именно так
Скриншот снимается покадрово: страница прокручивается командой `window.scrollTo`
через CDP, после каждого шага реальное значение `window.scrollY` **читается обратно**,
и кадр вставляется на строку `round(scrollY * scale)`. Совпадение координат
получается по построению, без поиска совпадений и без накопления ошибки.
Два отвергнутых варианта:
* **Template matching (старая реализация).** Высота результата непредсказуема и
никак не связана с высотой документа, из которой считались координаты хотспотов.
При неудачном совпадении код молча подставлял смещение 750 px.
* **`Page.captureScreenshot` с `captureBeyondViewport`.** Проверено на живом
приложении: карусель во второй строке не отрисовывается, вместо неё дублируется
верх страницы. Изображение 1080×2531 выглядит правдоподобно, но нижняя треть
не соответствует DOM.
---
## 🔍 4. Достоверность данных
Инструмент показывает числа аналитику, поэтому источник каждого числа обозначен явно.
### 4.1 Источник метрик — `public/data/metrics.json`, поле `source`
* `clickhouse` — выгружено `tools/fetch_clickhouse.py`;
* `demo` — демонстрационные значения, **не аналитика**.
Если ключа события нет в каталоге, у хотспота **нет блока `metrics`**, и интерфейс
пишет «Нет данных». Ноль в этом месте означал бы «кнопку не нажимают» — совсем другое
утверждение.
### 4.2 Достоверность ключа — поле `keyConfidence`
`observed` › `dom-attribute` › `manual` › `rule` › `guessed`.
Только `observed` означает, что ключ действительно снят с приложения.
**Важно.** Ключи вида `PAYMENTS_CLICK`, `SERVICES_CLICK`, `MENUCLICKED` в поставляемой
разметке имеют статус `rule` — это соглашение проекта. Реальные ключи, снятые с живого
приложения 2026-08-24, выглядят иначе:
```
POST https://api.amplitude.com/
POST https://mc.yandex.ru/watch/96490559/1?page-url=goal://customer.telecom.kz/HOMEPAGEPAYMENTS
POST https://mc.yandex.ru/watch/96490559/1?page-url=goal://customer.telecom.kz/OPENWINDOWPAYMENT
```
То есть `HOMEPAGEPAYMENTS`, а не `PAYMENTS_CLICK`. Перевести ключи в `observed`:
```bash
py tools/monitor_live_events.py --seconds 120 --map main_dashboard
```
---
## 🔑 4.3 Реальные ключи событий, снятые с приложения
36 ключей подтверждено на устройстве. Каждый снят с провода
(`amplitude` — тело запроса к `api.amplitude.com`), ни один не выведен из подписи.
| Экран | Элемент | Ключ |
|---|---|---|
| `main_dashboard` | AITU Music Слушайте без ограничений | `aitu_music_banner_clicked` |
| `main_dashboard` | NEW TURBO Увеличьте скорость интернета | `turbo_widget_clicked` |
| `main_dashboard` | TV+ Смотрите все новинки | `HOMEADDSERVICETVPLUS` |
| `main_dashboard` | Telecom Shop Покупайте выгодно | `OPENINGWINDOWTELECOMSHOP` |
| `main_dashboard` | Заявки | `HOMETAPORDERS` |
| `main_dashboard` | Лицевой счет 11440366 г.Алматы, прс.До | `SELECTACCOUNT` |
| `main_dashboard` | Меню (правый навбар) | `notautho_main` |
| `main_dashboard` | Мои услуги | `HOMETAPMYSERVICES` |
| `main_dashboard` | Платежи | `HOMEPAGEPAYMENTS` |
| `main_dashboard` | Подключить интернет Скидки и бонусы | `fd_auth_tariffs_opened` |
| `main_dashboard` | Свободные средства 16 484.62 ₸ | `detalization_widget_open` |
| `main_dashboard` | Трафик | `HOMEDETALIZATION` |
| `main_dashboard` | Удв. лич. | `ID_CARD_HOME_CLICK` |
| `orders_screen` | Создать заявку | `ORDERSOPENSCREENCREATEORDER` |
| `payments_screen` | История платежей | `PAYMENTTAPPAYMENTSTORIE` |
| `payments_screen` | Мой автоплатеж | `PAYMENTSTAPMYAUTOPAYMENT` |
| `services_screen` | Подробнее | `click_details_mp` |
| `side_menu` | Заявки | `ORDERS` |
| `side_menu` | Мои услуги | `MYSERVICES` |
| `side_menu` | Оферта | `IMPORTANTDOCUMENTS` |
| `side_menu` | Платежи | `PAYMENTS` |
| `side_menu` | Помощь | `HELP` |
| `side_menu` | Профиль телеком | `PROFILETELECOM` |
| `side_menu` | Трафик | `DETALIZATION` |
| `tariff_bereket` | Подробнее о скидке | `fd_auth_bereket_constructor_opened` |
| `tariff_internet_200` | Подробнее о скидке | `fd_auth_internet200_constructor_opened` |
| `tariff_internet_500` | Подробнее о скидке | `fd_auth_internet500_constructor_opened` |
| `tariff_keremet_mobile` | Подробнее о скидке | `fd_auth_keremet_mobile_constructor_opened` |
| `tariff_keremet_tv` | Подробнее о скидке | `fd_auth_keremet_tv_constructor_opened` |
| `tariffs_full_digital` | Подробнее | `constructor_opened` |
| `tariffs_full_digital` | Подробнее | `constructor_opened` |
| `tariffs_full_digital` | Подробнее | `constructor_opened` |
| `tariffs_full_digital` | Подробнее | `constructor_opened` |
| `tariffs_full_digital` | Подробнее | `constructor_opened` |
| `traffic_screen` | Домашний интернет | `button_click_home_internet` |
| `traffic_screen` | Мобильная связь | `DETALIZATIONMOBILEINTERNET` |
### Что из этого важно
* **Ни один ключ не совпал с тем, что предлагало правило по подписи.** «Трафик» —
это `HOMEDETALIZATION`, «Заявки» — `HOMETAPORDERS`, «Свободные средства» —
`detalization_widget_open`.
* **Один и тот же раздел из разных точек входа даёт разные ключи.** Плитка на
главном экране и строка бокового меню ведут на один маршрут, но событие разное:
| Раздел | Плитка на главной | Строка бокового меню |
|---|---|---|
| Платежи | `HOMEPAGEPAYMENTS` | `PAYMENTS` |
| Мои услуги | `HOMETAPMYSERVICES` | `MYSERVICES` |
| Трафик | `HOMEDETALIZATION` | `DETALIZATION` |
| Заявки | `HOMETAPORDERS` | `ORDERS` |
Это ровно та разница, ради которой инструмент и делался: по одному ключу
«переход в Платежи» нельзя понять, откуда пришёл пользователь.
* **Именование в приложении несогласованное**: `HOMETAPORDERS` рядом с
`turbo_widget_clicked`, `button_click_home_internet` и `click_details_mp`.
* **Не инструментированы**: «Сервисы», «QR», «Мои бонусы» — переходят на свой
маршрут, но не отправляют события.
---
## 🗺️ 4.4 Инвентарь экранов
| Экран | Маршрут / поверхность | Зон | Подтверждено |
|---|---|---|---|
| `main_dashboard` | / | 24 | 13 |
| `services_screen` | /services | 7 | 1 |
| `traffic_screen` | /detalization | 10 | 2 |
| `payments_screen` | /payments | 10 | 2 |
| `orders_screen` | /appeals | 7 | 1 |
| `music_screen` | native | 20 | 0 |
| `side_menu` | / | 15 | 7 |
| `payments_auto` | /payments/auto | 7 | 0 |
| `payments_history` | /payments/history | 7 | 0 |
| `detalization_internet` | /detalization/internet | 7 | 0 |
| `detalization_mobile` | /detalization/mobile | 7 | 0 |
| `appeals_create` | /appeals/create-appeal | 13 | 0 |
| `service_details` | /ont/3608039 | 8 | 0 |
| `profile_settings` | /settings | 10 | 0 |
| `balance_detail` | /detalization/balance/current | 12 | 0 |
| `bonuses_screen` | /bonuses | 7 | 0 |
| `qr_scan` | /scan-qr | 6 | 0 |
| `id_card` | /digital-document/id-card | 8 | 0 |
| `services_catalog` | /extra-services | 12 | 0 |
| `tariffs_full_digital` | /full-digital | 16 | 5 |
| `tv_plus` | /tv-plus | 14 | 0 |
| `turbo_landing` | /turbo | 7 | 0 |
| `help_screen` | /help | 10 | 0 |
| `important_docs` | /important-docs | 9 | 0 |
| `aitu_music` | /aitu/music | 12 | 0 |
| `tariff_bereket` | /full-digital/auth/tariff | 10 | 1 |
| `connect_service` | /full-digital/auth/connect-service | 10 | 0 |
| `tariff_keremet_mobile` | /full-digital/auth/tariff | 10 | 1 |
| `tariff_keremet_tv` | /full-digital/auth/tariff | 10 | 1 |
| `tariff_internet_500` | /full-digital/auth/tariff | 10 | 1 |
| `tariff_internet_200` | /full-digital/auth/tariff | 10 | 1 |
| `account_selector` | / | 11 | 0 |
Названия маршрутов не совпадают с подписями: «Трафик» → `/detalization`,
«Заявки» → `/appeals`, «Подключить интернет» → `/full-digital`,
«Удв. лич.» → `/digital-document/id-card`.
### Три режима съёмки
* **обычный** — сшивка по прокрутке для WebView-страниц;
* **`--no-scroll`** — один экран без прокрутки; нужен для оверлеев: боковое меню
закрывается при `window.scrollTo`;
* **`--native`** — экран без WebView (Музыка): разметка из `uiautomator`.
### Заметки по подэкранам
* **Один ключ на пять кнопок — это не ошибка.** Все пять «Подробнее» в каталоге
тарифов отправляют `constructor_opened`; различить тариф можно только по
следующему событию — `fd_auth_<тариф>_constructor_opened` у «Подробнее о скидке».
* **Экраны с задержкой отрисовки.** «Удв. лич.» и каталог тарифов рисуют скелетон
и наполняют его через 4-5 секунд. Съёмка ждёт, пока страница перестанет меняться
(`--wait`, по умолчанию 20 с); в разметке остаётся флаг `renderSettled`.
* **Выбор лицевого счёта** — это нижняя шторка на маршруте `/`, а не отдельный
маршрут. Снимается с `--no-scroll`.
* **Повторяющиеся подписи.** На экране может быть пять кнопок «Подробнее»; обход
адресует их по порядковому номеру, иначе все пять кликают в первую.
* **`/full-digital/auth/tariff`** — один маршрут для всех тарифов, поэтому каждый
снят под своим id (`tariff_bereket`, `tariff_keremet_mobile`, ...).
### Ограничения перехвата
* **Нативные вкладки** (Кабинет / TV+ / Музыка / Чаты / Бизнес), **аватар** и
**уведомления** отправляют события из нативного SDK. Через WebView их не видно.
* **Экран выбора лицевого счёта** отправляет `SELECTACCOUNT`, но затем требует
пин-код — снять его скриншот автоматически нельзя.
* **Цепочки событий.** Одно нажатие порождает несколько событий; при частых
нажатиях позднее событие приписывается следующему элементу. Пауза 5+ секунд
и повтор в двух прогонах.
---
## ⚡ 5. Функционал
1. **Симулятор** — мокап смартфона с прокруткой, переходами и историей «Назад».
Переходы на несуществующие экраны не «проглатываются»: такая зона не кликабельна.
2. **Тултипы метрик** — ключ события, русское название, число событий, уникальные
пользователи, события на пользователя, доля кликов, целевой экран; плюс пометки
«демо-значения» и «ключ не проверен».
3. **Тепловая карта** — нормировка только по измеренным зонам; зоны без метрик серые.
4. **Редактор разметки** — координаты, ключ события с автодополнением из каталога,
целевой экран. **Сохранение на диск** через `/api/save-app-map` (с резервной копией).
5. **Съёмка экрана с телефона** — полная прокрутка, нативная разметка из `uiautomator`,
DOM-элементы из CDP, обрезка выходящих за экран каруселей, дедупликация.
6. **Перехват событий** — `fetch`, `XMLHttpRequest`, `sendBeacon` и (как запасной
источник) analytics-connector; плюс цель Яндекс.Метрики как подтверждение.
7. **Автообход** — переход по разделам через DOM, съёмка каждого, запись пойманных
ключей.
8. **Индикатор устройства** — различает «телефон не подключен», «нет WebView» и
«сервер не отвечает».
---
## 🛠️ 6. Результаты аудита: что было сломано и как исправлено
### 6.1 Координаты хотспотов не совпадали с картинкой
Разметка считалась в координатах документа WebView
(`docHeight = scrollHeight * scale + 455`), а картинка бралась либо одиночным
скриншотом 2340 px, либо непредсказуемой сшивкой. На момент аудита
`main_dashboard` имел `totalHeight: 3886` при изображении высотой 2340: нижнее меню
и часть кнопок оказывались за пределами картинки и были недоступны.
*Исправлено:* детерминированная сшивка по прочитанному `scrollY` (§3). Проверено:
изображение 1080×2989, все 24 зоны совпадают с элементами.
### 6.2 `live_auto_recorder.py` игнорировал аргумент
Скрипт читал `sys.argv`, но `__main__` вызывал
`sync_current_screen_from_webview("main_dashboard")` жёстко. Любой запрос
`/api/live-sync-webview?screenId=…` перезаписывал главный экран. Воспроизведено:
запрос для `services_screen` изменил `main_dashboard`.
*Исправлено:* аргументы через `argparse`, валидация `screenId`.
### 6.3 Жёстко зашитый PID сокета WebView
`webview_devtools_remote_18732` в двух скриптах. PID меняется при каждом перезапуске
приложения.
*Исправлено:* `find_webview_socket()` — `pidof` с проверкой по `/proc/net/unix`.
### 6.4 Выдуманные метрики выдавались за аналитику
`totalEvents: 1200 + len(hotspots) * 410` в трёх скриптах; `metrics.json` целиком
синтетический; `fetch_clickhouse.py` указывал на `http://100.x.x.x:8123`.
*Исправлено:* генерация чисел удалена; `attach_metrics()` либо ставит реальную строку
каталога, либо не ставит ничего; каталог помечается `source`; коннектор ClickHouse
переписан на серверные параметры (`{name:Type}`) вместо подстановки в SQL.
### 6.5 Перехват событий не работал
Хук ставился на `analyticsConnectorInstances['$default_instance'].eventBridge`.
Проверено: хук цепляется (две инстанции, обе с `setEventReceiver`), но **не срабатывает
ни разу** — приложение отправляет события напрямую HTTP-запросом на
`api.amplitude.com`.
*Исправлено:* `tools/amplitude_hook.py` оборачивает `fetch`, `XMLHttpRequest` и
`sendBeacon`, разбирает `events[].event_type` из payload Amplitude HTTP V2 и
дополнительно снимает цель Яндекс.Метрики. Именно так были получены реальные ключи
из §4.2.
### 6.6 API возвращал успех после неудачи
`/api/capture-adb` читал `${screenId}_result.json` от предыдущего запуска и возвращал
его даже при падении Python-скрипта — интерфейс показывал «✅ обновлено» со старыми
данными.
*Исправлено:* решение принимается по JSON-вердикту скрипта; ошибка отдаётся с 502.
### 6.7 Подстановка `screenId` в командную строку
`exec(\`py tools/capture_screen.py ${screenId} …\`)` — параметр запроса попадал в
шелл.
*Исправлено:* `execFile` с массивом аргументов плюс проверка `^[A-Za-z0-9_-]{1,64}$`
на обеих сторонах.
### 6.8 Гонка записи
Middleware запускал `capture_screen.py` и `live_auto_recorder.py` одновременно, оба
писали `telecomkz_app_map.json`.
*Исправлено:* очередь на сервере, атомарная запись через временный файл.
### 6.9 Правки редактора нельзя было сохранить
Существовал только экспорт файла в «Загрузки»; всё, что редактировалось, терялось при
перезагрузке, а Live-синхронизация раз в 2 секунды затирала форму.
*Исправлено:* эндпойнт `/api/save-app-map` с резервной копией, кнопка «Сохранить
разметку на диск», индикатор несохранённых изменений; Live-синхронизация
приостанавливается в режиме редактора и при несохранённых правках.
### 6.10 Ошибки CDP-протокола во всех скриптах
`await ws.recv()` после `Page.enable` / `Network.enable` считался ответом на команду,
хотя CDP присылает события вперемешку с ответами; целевая страница выбиралась как
`pages[0]` без проверки типа.
*Исправлено:* `CdpSession.send()` сопоставляет ответы по `id` и буферизует события;
`pick_page_target()` фильтрует по `type == "page"` и предпочитает `customer.telecom.kz`.
### 6.11 Найдено при доснятии остальных экранов
* **Повторная съёмка стирала подтверждённые ключи.** `capture()` полностью заменяет
хотспоты экрана, поэтому каждый пересъём терял все `observed`-ключи. Добавлен
перенос доверенных ключей (`observed` / `manual` / `dom-attribute`) по подписи
элемента: `telecom_cdp._merge_trusted_keys()`.
* **Оверлей дублировался на скриншоте.** Боковое меню — `position: fixed` поверх
документа, который всё ещё сообщает о прокрутке: `scrollY` менялся, пиксели — нет,
и второй кадр вклеивался ниже, повторяя интерфейс. Теперь кадры сравниваются, и
сшивка останавливается, как только картинка перестаёт меняться.
* **Скрытые за оверлеем элементы попадали в разметку.** Добавлена проверка
`document.elementFromPoint` по центру видимой части элемента. Для прокручиваемых
экранов она применяется только к полностью видимым элементам — иначе карточки под
«липким» баннером на нулевой прокрутке отбрасывались бы, хотя ниже по сшитому
скриншоту они видны.
* **Боковое меню отсутствовало в разметке.** Его строки — обычные `div.nav-link`
без `role` и `href`, ни один селектор их не покрывал.
* **Совпадающие ключи.** «Мой автоплатеж» и «История платежей» оба содержат
«платеж» и получали один `PAYMENTS_CLICK`. Ключи на экране теперь уникальны.
* **Блокировка экрана портила данные.** Заблокированный телефон продолжает отвечать
на `screencap` и `uiautomator`, поэтому съёмка молча записывала экран блокировки
поверх настоящего. Добавлена проверка `require_awake()`.
* **Обрыв CDP терял собранное.** Переход на экран, заменяющий WebView, рвал сокет, и
`ConnectionClosedError` уходил мимо обработчика — прогон падал вместе с уже
пойманными событиями. Теперь это `DeviceError`, и монитор завершается штатно.
---
### 6.12 Прочее
* Замороженный процесс приложения не отвечает по сокету — добавлена проверка
переднего плана и вывод приложения на экран перед подключением.
* `build_smart_hotspots()` игнорировала снятый дамп `uiautomator` и возвращала
захардкоженную разметку главного экрана для **любого** `screenId`. Теперь нативные
элементы берутся из дампа по `resource-id`, включая активную вкладку
(`clickable="false"` у выбранного таба).
* Кириллические ключи (`CLICK_ЧАТЫ`) заменены транслитерацией.
* Элементы горизонтальных каруселей выходили за правый край (x + width = 1347 при
ширине 1080) — теперь обрезаются по канве.
* Порядок правил классификации: «Подключить интернет Скидки и бонусы» попадал в
`BONUSES_CLICK`; правила переупорядочены от частных к общим.
* Пути в скриптах резолвятся от корня проекта, а не от текущего каталога.
* 11 дублирующих скриптов перенесены в `tools/legacy/`, на их местах — заглушки,
перенаправляющие на рабочие реализации.
---
## 📂 7. Структура проекта
```
telecomkz_scraper/
├── public/
│ ├── assets/screens/ скриншоты + *_result.json
│ └── data/
│ ├── telecomkz_app_map.json экраны, зоны, связи
│ └── metrics.json каталог метрик с полем source
├── src/
│ ├── components/
│ │ ├── PhoneSimulator.tsx мокап, оверлеи, тепловая карта
│ │ ├── HotspotTooltip.tsx карточка метрик + пометки достоверности
│ │ ├── GuiEditor.tsx редактор зон
│ │ └── Sidebar.tsx экраны, режимы, съёмка, сохранение
│ ├── types/simulator.ts типы + KeyConfidence / MetricsSource
│ └── App.tsx контроллер
├── tools/
│ ├── telecom_cdp.py ядро: ADB, CDP, геометрия, app map
│ ├── amplitude_hook.py перехват событий в странице
│ ├── capture_screen.py съёмка + разметка
│ ├── live_auto_recorder.py пересборка хотспотов экрана
│ ├── monitor_live_events.py живой монитор событий
│ ├── crawl_and_map_all.py автообход
│ ├── fetch_clickhouse.py метрики
│ ├── reclassify_hotspots.py пересчёт ключей без телефона
│ ├── inspect_page.py диагностика WebView
│ ├── device_status.py статус устройства
│ └── legacy/ старые версии, не используются
├── vite.config.ts dev-сервер + API-мост
└── .env.example настройки ClickHouse
```
---
## 🚀 8. Запуск и типовые сценарии
```bash
npm install
py -m pip install opencv-python numpy pillow websockets requests
npm run dev # http://localhost:5173
```
**Проверить связь с телефоном**
```bash
py tools/device_status.py
```
**Снять текущий экран**
```bash
py tools/capture_screen.py main_dashboard --name "Главный экран (Кабинет)"
```
**Узнать настоящие ключи событий** (главный сценарий для аналитика)
```bash
py tools/monitor_live_events.py --seconds 120 --map main_dashboard
```
**Подтянуть метрики**
```bash
cp .env.example .env # указать CLICKHOUSE_HOST
py tools/fetch_clickhouse.py --days 30
py tools/reclassify_hotspots.py
```
**Диагностика WebView**
```bash
py tools/inspect_page.py amplitude bridge vue
```
### Известные ограничения
* Если приложение уходит в фон, Android замораживает процесс и отладочный сокет
перестаёт отвечать. Инструменты сами выводят приложение на передний план.
* Ключи событий в поставляемой разметке имеют статус `rule` и требуют подтверждения
через `monitor_live_events.py` — до этого join с ClickHouse не даст результата.
* `metrics.json` в репозитории помечен `source: "demo"`.