# Merchant HUD And Location Capture Implementation Plan **Intent:** Дать игроку спокойную оперативную обратную связь поверх окна Lineage II и позволить вручную сохранить примерное место торговца вместе с посещением магазина. **Current Behavior:** Состояние распознавания видно только в большом окне аудита. Превью уже умеет рисовать найденные области и прогресс иконок, но при возврате в игру оно скрыто. Сервер сохраняет OCR-наблюдения и общую калибровку, но отдельного посещения магазина и изображения места нет. **Expected Outcome:** Из аудита открывается один компактный always-on-top HUD. Он использует уже существующее состояние выбранного источника и показывает торговца, сторону сделки, прогресс предметов и последнюю распознанную позицию. Пользователь может снять текущий кадр, увидеть сжатую миниатюру и явно сохранить её как место текущего посещения магазина. **Target-Perspective Output:** Игрок возвращается в Lineage II и поверх окна видит `Покупка · Dwa`, сетку с найденными и ожидающими иконками и последнюю цену. После `Снять место` он видит миниатюру, подтверждает сохранение и получает устойчивый статус `Место сохранено`. **Truth Owner:** Браузерный `result` владеет только живым состоянием HUD и текущим `shopVisitId`. PostgreSQL в `home-service` владеет сохранённым посещением и его JPEG. Каталог `home-service` остаётся владельцем канонических имён и иконок предметов. **Contract Boundary:** Parser отправляет authenticated same-origin JSON с UUID посещения, торговцем, стороной, временем, исходными размерами и JPEG data URL. Каждое OCR-наблюдение получает необязательный `shopVisitId`, чтобы связать товары с тем же посещением. **Cutover:** HUD читает тот же `result`, что текущие source preview и results; отдельного второго состояния распознавания не создаётся. После появления HUD существующие маленькие status chips в source preview остаются fallback внутри страницы, но перестают быть основным игровым feedback path. **Displaced Path:** Постоянное переключение из игры в большую вкладку `Результат` для контроля процесса демотируется до подробной диагностики. Автоматическая или покадровая запись мест не добавляется. **Value Density:** Первый срез использует нативный Document Picture-in-Picture вместо Electron/Tauri и сохраняет только один подтверждённый сжатый кадр на посещение. **Acceptance Evidence:** В поддерживаемом Chrome/Edge игрок открывает HUD, возвращается в игру, видит неизменного торговца при tooltip, наблюдает переход одного слота из ожидания в найденный, снимает место и после server acknowledgement видит сохранённый статус. В PostgreSQL находится одна строка посещения с тем же `shopVisitId`; связанное наблюдение содержит тот же ID. **Evidence Lane:** Автоматически: parser unit tests, HUD model tests, contracts/backend route tests, migration smoke, parser/frontend/backend builds и `git diff --check`. Вручную только после отдельного разрешения: реальное always-on-top поведение над Lineage II, читаемость и полезность сжатого снимка. **Kill Criteria:** Не создавать второй recognition loop, не копировать OCR-состояние в независимый store, не сохранять full-resolution кадры, не снимать экран автоматически, не добавлять native wrapper до доказанного ограничения PiP. Если требуется несколько независимых overlay одновременно или click-through поверх каждого окна игры, остановить web-срез и отдельно спланировать Windows companion. **Architecture Slice:** `l2-market-parser` владеет HUD, visit lifecycle и компрессией кадра; shared contract, BFF, Nest service и PostgreSQL table живут в `home-service`; ingress меняется только для bounded screenshot body. **Plan Review Gate:** Requires PRE review before execution. ## Physical Scene And Interface Contract Игрок работает вечером на большом мониторе с несколькими оконными клиентами Lineage II. HUD лежит поверх текущего клиента и должен читаться на любом игровом фоне, не требуя переключения внимания. - Один solid dark utility surface примерно `320 × 420`, без прозрачного стекла, игровых рамок и декоративной анимации. - Верхняя строка: выбранный источник и состояние сбора. - Главная строка: `Покупает/Продаёт · торговец`. Она занимает фиксированное место и не прыгает при OCR. - Сетка `6 × 3`: каноническая иконка для найденного предмета, нейтральная ячейка для ожидающего, понятный `?` для неоднозначного. - Последний результат: название, количество, цена и короткий статус совпадения с каталогом. - Действие `Снять место` создаёт inline-миниатюру. Рядом появляются `Сохранить` и `Переснять`; modal не используется. - Успех, ошибка и ожидание меняют текст/цвет в фиксированном status slot. Движение только для смены состояния, 150–200 ms, без layout animation. - Document PiP открывается только по нажатию пользователя и только в secure context. Если API недоступен, большая страница продолжает работать и объясняет, что нужен актуальный Chrome/Edge. - Web-страница не может задавать позицию PiP и поддерживает только одно такое окно. Пользователь размещает его сам; HUD показывает выбранный источник. ## Architecture Map ### Files to create `l2-market-parser`: - `src/market-hud.js`: одно представление HUD из существующего `result`, открытие/закрытие Document PiP и fallback-status. - `test/market-hud.test.js`: модель торговца, прогресс слотов, последний предмет и unsupported fallback. `home-service`: - Следующая свободная `backend/migrations/*.sql`: таблица `l2_market_shop_visits`. - Соответствующий `backend/migrations/meta/*_snapshot.json` и `_journal.json` только через текущий Drizzle workflow. - `frontend/src/app/api/l2/market/visits/route.ts`: authenticated same-origin BFF `PUT`. ### Files to modify `l2-market-parser`: - `index.html`: кнопка `Поверх игры` и минимальные fallback/status hooks. - `src/main.js`: передача выбранного `result` в HUD, lifecycle одного `shopVisitId`, захват текущего источника и явное сохранение миниатюры. - `src/frame-analyzer.js`: новый visit ID при подтверждённой смене/повторном открытии магазина; очистка ID вместе с transient shop progress; добавление ID в observation. - `src/style.css`: общие HUD tokens и компактная layout-сетка без отдельной визуальной системы. - `src/parser.js`: чистый helper экспорта HUD/visit данных, только если он уменьшает дублирование. `home-service`: - `contracts/src/l2.ts`: strict `L2MarketShopVisitSchema`, bounded JPEG data URL, размеры и optional `shopVisitId` у observation. - `backend/src/database/schema.ts`: `l2MarketShopVisits` с UUID, временем, source, side, merchant, dimensions, bounded image text и indexes. - `backend/src/l2/market/l2-market.controller.ts`: `PUT /l2/market/visits/:id`. - `backend/src/l2/market/l2-market.service.ts`: idempotent insert/update одного пользовательски подтверждённого кадра посещения. - `backend/src/l2/market/l2-market.service.spec.ts` и controller spec: strict validation, idempotency и linkage ID. - `frontend/src/lib/l2-session.spec.ts`: unauthenticated/cross-origin rejection и authenticated forwarding. - `docs/goals/l2-craft-farm-planner/l2-vault.nginx`: exact visits route с bounded body limit только если итоговый JSON превышает текущие 16 KiB. - `frontend/public/l2/market-audit/**`: только сгенерированный production bundle после parser build. ### Files to avoid - Не менять catalog matching, OCR, calibration geometry и quote/snapshot semantics. - Не хранить изображение внутри каждого item observation или каждого retry batch. - Не трогать другие home-service сайты, admin access и backend public port. - Не добавлять Electron, Tauri, React/state library, object storage или image processing dependency. ### Source of truth and paths - **Read path:** `state.results` → `buildHudModel(result)` → один PiP document. Каталожные имена/иконки уже приходят через существующий fuzzy resolver. - **Screenshot path:** выбранный source video → canvas downscale → JPEG quality reduction → inline preview → explicit confirm → BFF → Nest → PostgreSQL. - **Observation link:** `activateShop` создаёт `shopVisitId`; каждое последующее observation этого магазина несёт ID; закрытие магазина после существующего miss policy очищает ID. - **Compression ceiling:** начать с max width `640`, JPEG quality `0.35`; при превышении contract size уменьшать quality/width до bounded payload. Сохранять исходные и итоговые размеры. - **Write semantics:** повторный `PUT` с тем же visit ID заменяет только подтверждённую миниатюру и capture metadata; OCR observations остаются append-only batch payloads. ## Tasks ### 1. Зафиксировать visit lifecycle и shared contracts **Allowed scope:** `src/frame-analyzer.js`, parser tests, `contracts/src/l2.ts`, market service tests. - Добавить `shopVisitId` и `shopVisitStartedAt` в transient result. - Создавать ID только при первом подтверждённом заголовке после reset или реальной смене shop key. - Не менять ID, пока tooltip закрывает заголовок. - Добавить optional ID в observation и strict visit screenshot contract. **Verification:** `npm test`; focused contracts build and market specs. **Acceptance:** тест воспроизводит header → tooltip → несколько items и доказывает один visit ID и неизменного торговца. ### 2. Построить один PiP HUD из существующего result **Allowed scope:** `market-hud.js`, `main.js`, `index.html`, `style.css`, HUD test. - Открывать Document PiP только по явному нажатию. - Подключить модель выбранного source result без второго timer/recognition loop. - Рендерить merchant, side, slot progress, icons, unresolved state и last item. - Перерисовывать HUD после текущего `drawSourceOverlay`/result update. - Закрытие PiP не останавливает сбор; закрытие основной страницы закрывает PiP нативно. **Verification:** parser unit tests and production build. **Acceptance:** mocked supported/unsupported window tests; модель сохраняет фиксированную геометрию при idle/found/error. ### 3. Добавить ручной capture с inline preview **Allowed scope:** parser HUD/main code and tests. - `Снять место` доступно только при active source и подтверждённом merchant/visit ID. - Захватить один текущий frame, downscale/compress без новой зависимости. - До отправки показать миниатюру, размер и `Сохранить/Переснять`. - После ошибки оставить миниатюру для повторного сохранения; не делать background screenshots. **Verification:** deterministic canvas compression test with a generated fixture; payload always satisfies contract limit. **Acceptance:** одна кнопка создаёт один preview; cancel/reshoot не пишет сервер. ### 4. Сохранить место торговца в home-service **Allowed scope:** migration/schema, shared contract, market controller/service, exact BFF route and tests. - Создать bounded visit table and index by merchant/seenAt. - Реализовать authenticated same-origin `PUT` and idempotent backend write. - Не принимать PNG, произвольный MIME, oversized base64 или mismatched path/body ID. - Не возвращать изображение из unrelated snapshot/catalog endpoints. **Verification:** Drizzle migration generation/check, market service/controller specs, frontend BFF test, contracts/backend/frontend builds. **Acceptance:** database smoke verifies one replaceable visit row and observation payload with the same `shopVisitId`. ### 5. Интегрировать и доказать cutover **Allowed scope:** generated audit bundle, relevant README/evidence, exact ingress body rule if required. - Пересобрать parser directly into `frontend/public/l2/market-audit`. - Проверить bundle на отсутствие embedded tokens/full-resolution fixtures. - Programmatic public route check remains password-gated. - Не объявлять игровой overlay доказанным без отдельного manual permission. **Verification:** both repository builds, focused tests, `git diff --check`, migration smoke. **Acceptance:** automated lane green; target lane recorded as either proven over a real game window or `implemented but visually unproven`. ## PRE Self-Review - **Mode:** PRE - **Verdict:** aligned - **Blockers:** none for the web prototype. - **Major constraint:** Document PiP gives one user-positioned always-on-top window, not multiple click-through overlays attached to game windows. This is explicit in UX and kill criteria. - **Major security constraint:** screenshots may contain chat/account information; capture stays manual with inline preview, strict JPEG/size validation and authenticated storage. - **Smallest next gate:** owner accepts the one-PiP limitation and manual screenshot confirmation, then execution starts with Task 1.