Commit Graph

196 Commits

Author SHA1 Message Date
wasrusgen
ee619bb57d debug: log auth hash mismatch + strip token 2026-05-12 21:18:34 +03:00
wasrusgen
d293ded525 debug: add entry+auth prints 2026-05-12 20:59:04 +03:00
wasrusgen
a0c02110d4 debug: use print to stderr — logger overridden by uvicorn 2026-05-12 20:31:35 +03:00
wasrusgen
2308697e55 debug: log /api/me decisions (tg_id, admin_id, role, roles) 2026-05-12 20:25:54 +03:00
wasrusgen
865c3eaf40 fix: после grant_role существующий dict не обновлял поле role
При входе менеджером (?role=manager) get_or_create_user обновлял CSV
в Sheets, но в памяти существующего dict обновлял только roles[],
а старое поле role оставалось со старым значением. _handle_me читал
role и не находил 'manager' → fallback на client cabinet.

Теперь после grant_role перечитываем строку из Sheets и обновляем
оба поля (role + roles). Плюс в _handle_me предпочитаем roles[]
если он уже распарсен.
2026-05-12 20:18:10 +03:00
wasrusgen
4848b3a3ef fix: splash блокировал клики во время fade-out + сократил min-show
1. .loader.splash.hide теперь имеет pointer-events:none !important —
   во время 400мс fade splash не блокирует тапы.

2. minShow 2500мс → 1200мс — меньше ожидания, меньше шансов попасть
   в окно когда splash ещё блокирует.

Cache bust v=20260513h.
2026-05-12 20:16:59 +03:00
wasrusgen
67034e011a workflow B: заявка на замер от менеджера → инбокс замерщика → завершение
End-to-end поток:

1. Менеджер на главной тапает «Заказать замер» → #/request → форма:
   ФИО · телефон · адрес · dropdown «Кому назначить» · заметки.
   Submit → POST /api/measurement_request → строка в Measurements
   со status=requested + assigned_to_tg_id. Бот шлёт DM замерщику.

2. Замерщик открывает кабинет (?role=staff) → видит inbox с заявкой.
   Тап → #/inbox/<id> → карточка с реквизитами + поле datetime-local.
   Сохранить дату → POST /api/measurement_schedule → status=scheduled.
   Бот уведомляет менеджера.

3. В нужный день замерщик тапает «📐 Сделать замер сейчас» →
   wizard открывается в update-mode (#/measure?id=<id>), pre-fill
   client_name/phone из заявки, пропускает шаг «Клиент». После submit
   → backend обновляет ту же строку (status=completed) + DM менеджеру.

Backend changes:
- Расширена схема Measurements: assigned_to_tg_id, requested_by_tg_id,
  scheduled_at, address, client_name, client_phone (отдельные колонки).
  ensure_measurements_sheet() автоматически дополняет колонки.
- _handle_measurement переписан под 2 режима (create/update).
- 3 новые ручки: /api/measurement_request, /api/measurement_inbox,
  /api/measurement_schedule. Все с правильной проверкой ролей.
- Telegram-уведомления на каждом переходе статуса.

MiniApp:
- Новый модуль request.js — wizard заявки с dropdown замерщиков
  (грузится из /api/staff_list?role=measurer).
- renderStaff теперь грузит реальный инбокс из /api/measurement_inbox.
- renderInboxDetail — карточка заявки с datetime-picker.
- В quick-actions менеджера: «Заказать замер» (primary) +
  «Замер сейчас» (legacy direct fill).
- measurements.js поддерживает update-mode через ?id=.

Cache bust v=20260513g.
2026-05-12 20:00:16 +03:00
wasrusgen
d859e9791c roles: multi-role foundation (manager / client / measurer / assembler)
Users.role теперь хранит CSV-список ролей: 'manager,measurer'.
Парсим, добавляем, отзываем — все через sheets.parse_roles / grant_role /
revoke_role / list_users_with_role. Старые однострочные значения работают
как раньше (legacy compat).

Backend:
- /api/me возвращает roles[] (массив), role (главная для legacy-UI),
  plus capabilities {measurer, assembler} для staff
- /api/grant_role (admin-only) — добавить/отозвать роль
- /api/staff_list (manager-only) — список сотрудников по роли
  (будет использоваться в dropdown «выбрать замерщика»)
- При role=staff отдаём отдельный кабинет; если у юзера нет measurer/
  assembler — возвращаем error=no_staff_role

Bot:
- /start — 3-я reply-кнопка [🔧 Я сотрудник]. При тапе MiniApp получает
  ?role=staff и решает кабинет по capabilities.
- /whoami — сотрудник присылает свой Telegram ID, пересылает куратору
  чтобы тот выдал роль через /api/grant_role.

MiniApp:
- renderStaff() — заглушка кабинета сотрудника с шапкой (имя, аватар,
  список ролей) и пустым inbox («Пока пусто»). Если есть measurer —
  быстрая кнопка «Сделать новый замер».
- При error=no_staff_role — экран с инструкцией как получить роль.
- CSS .staff-head / .staff-no-role.

Cache bust v=20260513f.
2026-05-12 19:14:39 +03:00
wasrusgen
6d57372b0b splash: bump min display time to 2.5s
Cache bust v=20260513e.
2026-05-12 19:00:34 +03:00
wasrusgen
c767954535 splash: move out of #app + guarantee 700ms min visibility
Splash was nested inside <main id='app'> — render functions wipe
app.innerHTML before hideSplash() runs, so the splash never showed.
Moved it to a sibling of #app and made hideSplash() wait for at least
700ms of total time elapsed since page load before fade-out.

Cache bust v=20260513d.
2026-05-12 18:57:40 +03:00
wasrusgen
1ca8b3a5a1 bot: role buttons → MiniApp directly + branded splash loader
Bot: упрощён до одного шага — /start показывает 2 reply-кнопки
[👤 Я менеджер] [🏠 Я клиент], обе уже WebApp — открывают кабинет
сразу с нужным role= в query. Никаких промежуточных меню.

MiniApp: новый брендированный загрузочный экран с логотипом ZOV
(inline SVG, fill = walnut #6B4A2B), дыхательной анимацией 2.2s,
тонкой полоской прогресса и подписью «Открываем кабинет · ZOV».
Splash прячется (350мс минимум + fade-out) после рендера главного
экрана или маунта подэкрана (Podbor/Clients/Measurements).

Cache bust v=20260513c.
2026-05-12 18:54:09 +03:00
wasrusgen
8f6b5e56bb bot: 3-уровневое меню — роль → action → MiniApp
/start теперь показывает только две reply-кнопки внизу:
  [👤 Я менеджер] [🏠 Я клиент]

Тап «Я менеджер» → меню менеджера (4 ряда):
  🤖 Подбор техники | 📐 Новый замер   ← WebApp
  👥 Мои клиенты   | 🏠 Кабинет        ← WebApp
  ℹ️ Что умеет бот?| 📞 Куратор         ← текст
  📋 Чек-лист встречи | ⬅️ Сменить роль ← текст

Тап «Я клиент» → меню клиента (3 ряда):
  🏠 Мой кабинет  | 📐 Мой замер    ← WebApp
  📞 Связь с менеджером | ℹ️ О сервисе ← текст
  ⬅️ Сменить роль

«⬅️ Сменить роль» в любом меню → возврат к выбору роли.

Заменён inline-keyboard на reply-keyboard (постоянная панель снизу).
2026-05-12 18:46:30 +03:00
wasrusgen
b2438507c3 bot: persistent reply keyboard with WebApp buttons + info actions
Now after /start, manager sees a bottom keyboard (4 rows) for fast access:
  Row 1: 🤖 Подбор техники | 📐 Новый замер   ← WebApp
  Row 2: 👥 Мои клиенты    | 🏠 Кабинет       ← WebApp
  Row 3: ℹ️ Что умеет бот? | 📞 Куратор       ← text
  Row 4: 📋 Чек-лист встречи                  ← text

WebApp buttons jump straight to a MiniApp screen via ?go=<podbor|measure|clients>;
app.js parses ?go on load and pre-sets location.hash so the right module mounts.

Added /menu (re-show keyboard) and /hide (remove). Text buttons trigger
in-chat info responses (bot description, contact, meeting checklist).

Cache bust v=20260513b.
2026-05-12 18:37:24 +03:00
wasrusgen
a084542bbf measurements: photo upload + measurement detail page + PDF/print
Wizard: new 'photos' step (6 total) — camera/gallery input, client-side
canvas compression to 1600px @ ~78% JPEG, max 12 photos. Thumbnails
with delete in step; preview in summary.

Backend: POST /api/measurement now decodes data-URL photos and saves
to /app/photos/<id>/N.jpg (volume-mounted). New GET /api/photo/{id}/{n}
serves files with path-traversal protection. New POST /api/measurement_detail
returns full measurement record (walls/openings/photos/notes/...).

Clients page: measurement rows now clickable → renderMeasurement detail
view with key-value grid + photo gallery + 'Скачать PDF / Печать'.
Print stylesheet (@media print) hides navigation/buttons/uploaders and
prints clean A4-friendly layout.

Podbor report: existing 'Печать → PDF' now falls back to inline
window.print() inside Telegram WebApp (popups are blocked there).

Cache bust v=20260513a.
2026-05-12 18:11:29 +03:00
wasrusgen
10bcc75b13 measurements: kitchen layout wizard + 5 layout pictograms + profile integration
5 LAYOUT PICTOGRAMS (podbor.picts.js):
- linear: одна стена с гарнитуром
- l_shape: Г-образная, две стены с подсвеченным углом
- u_shape: П-образная, три стены
- island: линейный гарнитур + отдельный остров посередине
- peninsula: Г-образная + барная стойка-полуостров
Все в стиле D · top-down view, walnut stroke, теплые градиенты

MEASUREMENTS.JS WIZARD (5 шагов):
1. client_info — имя + телефон (валидация)
2. layout — pict-карточки 5 типов
3. size — длины стен (1-3 по layout), площадь, потолок (мм)
4. openings — окно / дверь / коммуникации / заметки
5. summary — обзор + Сохранить → POST /api/measurement

BACKEND (main.py):
- New /api/measurements (POST) для списка замеров менеджера
  с опц. фильтрами по client_tg_id
- _handle_measurement теперь дописывает имя+телефон клиента в notes
  (если client_tg_id не зарегистрирован — это новый клиент без аккаунта)
- handlers dispatcher: 'measurements' route added

ROUTING (app.js):
- Quick-action 'Новый замер' wired to '#/measure'
- routeByHash: Measurements.mount on #/measure

CLIENT PROFILE (clients.js):
- New section 'Замеры · N' on client history page
- fetchMeasurements() filters by client_tg_id or name match in notes
- layoutLabel() shows Russian label (Прямая / Угловая Г / etc.)
- Cache bump v=20260512c
2026-05-12 17:41:01 +03:00
wasrusgen
43c43af795 remove duplicate vent question + AI must propose charcoal filter on recirc hood
USER REPORT: 'после прогона всей техники, перед запросом опять вопрос про фильтр для вытяжки в конце'

ROOT CAUSE: Infra-шаг спрашивает 'Вытяжка → вентшахта?', хотя у hood-категории уже есть шаг 'Подключение' с вариантами Отвод/Рециркуляция/Универсальная. Это дубликат.

FIX:
- renderInfra: убран блок vent. Шаг показывается только если выбрана варочная.
- Auto-skip infra если нет hob (раньше требовался hob ИЛИ hood, теперь только hob)
- renderSummary: убрана строка 'Вентиляция'
- summaryBack: 'infra' только если cats.includes('hob')

AI PROMPT:
- Новый блок: режим вытяжки читать из per_cat.hood.answers.mode
- exhaust → обычная установка
- recirc → ОБЯЗАТЕЛЬНО упомянуть 'Угольный фильтр в комплекте/докупаем' в pros
  + в первой строке pros указать 'для квартир без вентшахты'
- combi → упомянуть универсальность
- 'Если recirc и фильтр не предложен — это ОШИБКА'

Cache: v=20260512b
2026-05-12 17:22:59 +03:00
wasrusgen
c4f3016b56 miniapp: client profile tab — list + history + lead detail
NEW FILE assets/clients.js:
- Clients.mount(container) — hash-routed view
  - #/clients              — list of all clients (cards: avatar, name, phone, leads count, last date)
  - #/clients/client/<key> — single client history (all leads as items)
  - #/clients/lead/<id>    — full lead detail with re-rendered report

UI:
- Card style: avatar with initial, name + phone, footer with N подборов + дата
- Pluralization for Russian (1 подбор / 2 подбора / 5 подборов)
- Date format: 'сегодня · 14:30' or 'DD.MM.YYYY'
- Status pills: new / sent / viewed / ordered

PODBOR.JS:
- Exposed renderSavedReport(ai, leadId) for Clients module reuse
- Same renderer as live podbor — same matrix, pros/cons, links

APP.JS:
- Quick action 'Клиенты' added (icon: user)
- Hash router: #/clients → Clients.mount()

INDEX.HTML:
- clients.js script added
- Cache bumped to v=20260512a

CSS:
- .client-list, .client-card with avatar+meta+footer
- .client-detail-head (big card with avatar 56px)
- .leads-list with .lead-item (grid: date | id | status | arrow)
- .loader-inline for async fetch
- .ai-text-fallback for legacy text-only responses
2026-05-12 07:20:54 +03:00
wasrusgen
9a2dcbc3fe backend: client profile API (/api/clients, /api/lead)
NEW HANDLERS:
- _handle_clients: groups manager's Leads by client (name or tg_id), returns
  list with leads_count, last_lead_at, client_phone (extracted from checklist),
  and full leads array per client. Sorted by recency desc.
- _handle_lead: fetches single lead with parsed checklist + ai_response JSON.
  Validates ownership (manager_tg_id matches caller).

NEW ENDPOINTS:
- POST /api/clients — list of manager's clients with summary
- POST /api/lead — single lead detail with ai_response for re-render

Both accept initData auth, only manager role can call.
Apps Script compat: ?path=clients and ?path=lead also work.
2026-05-12 07:16:14 +03:00
wasrusgen
efa2046a97 gitignore: cat_refresh.json 2026-05-12 07:10:06 +03:00
wasrusgen
fe472b0827 catalog: filter junk + background refresh + clear endpoint
FILTERING (catalog.py _save_results):
- CATEGORY_KEYWORDS: must contain category word ('холодильник', 'варочн', 'духов', etc.)
- CATEGORY_MIN_PRICE: filters parts/accessories (fridge >20k, hood >5k, etc.)
- PART_BLACKLIST: 'фильтр', 'лампочк', 'термодатчик', 'шланг', 'тэн', 'компрессор', etc.
- Previously had Asko light bulb (155₽), Miele dryer filter (376₽), Siemens cooktop in fridge category — all now filtered out

ASYNC REFRESH (main.py):
- POST /api/catalog/refresh queues background task, returns immediately
  (was sync, taking 3+ min → Cloudflare tunnel was killing connection)
- New GET /api/catalog/refresh_status for progress polling
- Concurrent refresh blocked (one at a time)

CLEAR ENDPOINT:
- POST /api/catalog/clear?cat=fridge clears one category
- POST /api/catalog/clear clears entire catalog (start over)

NEXT: clear current dirty data, re-seed fridge with filters
2026-05-12 07:09:33 +03:00
wasrusgen
9e652c4a34 catalog: models cache in Sheets — AI picks from real list, no SKU hallucination
NEW MODULE app/catalog.py:
- refresh_catalog(cats, sources, per_brand, delay) — runs parsers for seed brand+category pairs
- list_catalog(cat, tier, brand) — reads from Sheets
- list_for_ai(cats, tiers) — compact text for AI prompt context
- SEED_BRANDS_BY_TIER + CATEGORY_QUERIES — 22 brands × 8 cats = 176 combos
- Saves top-2 relevant results per (brand × cat), filters by brand presence in title
- Dedup by title hash within (cat, brand) bucket

SHEETS:
- ensure_sheet(name, headers) — auto-creates Catalog tab on first refresh
- Schema: id, category, brand, tier, model_name, search_query, price_min/max, image_url, source, url, last_seen_at

ENDPOINTS:
- POST /api/catalog/refresh?cat=X&per_brand=N — manual refresh (1 cat ~2-5 min)
- GET /api/catalog/list?cat=&tier=&brand= — read with filters
- GET /api/catalog/preview_ai?cats=fridge — debug what AI receives

AI PROMPT:
- Rule #0: if catalog passed in user prompt — MUST select only from there
- _build_catalog_context: filters by checklist.budget_preset → tier subset
  (luxe→premium, premium→premium+middle, middle→middle, budget→middle+budget)

_handle_podbor:
- Loads catalog subset, appends to user_prompt as 'ДОСТУПНЫЙ КАТАЛОГ МОДЕЛЕЙ'
- AI 'выбирай ТОЛЬКО из этого списка' rule reinforced

NEXT: trigger refresh manually for 1 category (~3 min), then real podbor test
to verify AI uses catalog models instead of hallucinating SKUs
2026-05-12 06:32:39 +03:00
wasrusgen
1a57374020 parsers: better image extraction — real product photos in report cards
CITILINK:
- Now reads data-src / data-original / srcset / src in priority order
- srcset → picks largest size variant (last in comma-list)
- Filters only _next/static/images (placeholder) and 'placeholder' in URL
- Accepts cs.citilink.ru / c.citilink.ru / images.citilink.ru product photos

ЯНДЕКС.МАРКЕТ:
- Collects all img attrs (data-src, data-original, srcset, data-srcset, src)
- Prefers avatars.mds.yandex.net (real product CDN), skips yastatic (icons/logos)
- Auto-appends /300x300 suffix to avatars.mds URLs without size

ENRICH_ONE (aggregator):
- Image picked by source priority: yamarket > wb > ozon > citilink > dns
- Yamarket photos are cleanest (avatars.mds.yandex.net)
- WB has product photos via basket-XX.wbbasket.ru
2026-05-11 23:43:25 +03:00
wasrusgen
0b48dd2371 simplify: remove 'features' step from all 8 categories
USER FEEDBACK:
'Особенности везде убрать, их можно в SWOT анализе приводить в качестве примечания
не акцентируя на них особого внимания. Современные фичи на 95% одинаковые.'

REMOVED features step from:
- fridge (NoFrost, Inverter, Wi-Fi, etc.)
- hob (Booster, FlexZone, FFD, Hob2Hood, etc.)
- oven (Wi-Fi, autoprogram, probe, softclose, etc.)
- dw (Wi-Fi, AutoOpen, AutoDose, AquaStop, Inverter)
- hood (touch, LED, auto, silent, turbo, wifi, perimeter)
- microwave (Wi-Fi, humid sensor, defrost, antibac)
- coffee (Wi-Fi, touch, grinder, autoclean)
- washer (inverter, steam, wifi, autodose, silent, aquastop)

KEPT: hood.color (about visible material/aesthetics, not feature)
KEPT: oven.location (where in kitchen — design-relevant)

NEW STEP COUNTS:
- fridge: 3 (was 4)
- hob: 4 (was 5)
- oven: 3-4 (was 4-5)
- dw: 3 (was 4)
- hood: 3-4 (was 4-5)
- microwave: 3 (was 4)
- coffee: 1-4 (was 2-5)
- washer: 5 (was 6)

AI PROMPT updated:
- Features no longer come from user — AI mentions important ones in highlights/pros
- Emphasis on MEASURABLE advantages in pros (N dB quieter, Y l more, N% cheaper)
- Не делать акцент на стандартных фичах — 95% одинаковые

USER WIZARD теперь короче и проще: тип → размер → ключевые параметры → готово
2026-05-11 23:37:41 +03:00
wasrusgen
cecb8d3444 review screen: fix text wrap + cleanup stale answers from removed steps
CSS:
- .rev-val: flex:1, min-width:0, overflow-wrap:break-word — длинные значения
  больше не ломаются мид-словом ('энергоэффективнос·...')
- .rev-label: max-width:40% — лейбл не съедает всё место
- hyphens:auto для перенос длинных слов на дефис

JS (getCatState):
- При загрузке per_cat фильтруем answers — оставляем только ключи которые
  есть в текущем config.steps
- Это убирает stale-поля типа 'class' у ПММ, оставшиеся в localStorage
  после рефакторинга шагов
- Безопасно: меняет только в памяти, не перезаписывает state (renderReview
  всё равно итерирует config.steps)
2026-05-11 23:25:23 +03:00
wasrusgen
6915bba845 user feedback: oven proportions + dw simplification
OVEN PICTOGRAMS (per user: 'духовка не очень похожа, прямоугольные, фасадом не закрываются'):
- oven_install_builtin: REMOVED dashed niche outline (ovens don't close with façade — sit in open cabinet)
- Made body wider+shorter — 78×74 viewBox area (was 68×112, too tall)
- Real 60×60 cm proportions, control panel at top + handle + glass window with racks
- oven_install_stove: тщательнее прорисован — cooktop (with concentric burners), control strip,
  oven door with handle + window, ножки чётче, линия пола

DW LOGIC SIMPLIFIED (per user: 'энергопотребление уже перебор'):
4 шага вместо 5:
1. Тип встройки (full/partial/freestanding) — was step 1, kept
2. Размер ширина (45/60) — was step 3, moved up to step 2
3. Корзины + программы — merged in one step:
   - 2 корзины · базовый (5-6 программ)
   - 3 корзины · стандарт  (8-10 программ)
   - 3 корзины · расширенный (12+ программ, стекло, авто, кастрюли)
4. Особенности (multi) — теперь содержит Wi-Fi, AutoOpen, AutoDose, Beam, AquaStop, ≤44dB,
   Inverter (включая A+++), GlassZone

Removed: separate 'class' step (energy efficiency moved into features as Inverter option)
2026-05-11 23:06:25 +03:00
wasrusgen
03c95fe13a wb: relevance filter — discard anti-bot trash products (платья/обувь in fridge search)
WB sometimes responds with 1-2 unrelated products instead of 429 status.
Was returning 'Платье вечернее' on 'Haier холодильник' query.

Fix: _is_relevant(product, query) checks that at least 1 significant query word (>=3 chars)
appears in product name or brand. Discards full result if zero matches.

Tradeoff: may sometimes reject valid product if query is overly specific (e.g. exact SKU).
But that's OK — we fall through to next query variant.
2026-05-11 23:02:37 +03:00
wasrusgen
d84a53f91d wb: skip proxy pool (use direct VPS-IP — residential were rate-limited) 2026-05-11 23:01:34 +03:00
wasrusgen
e9b0db6772 wb: API v9 → v18 (WB сменил endpoint и структуру) + brand+category fallback query
DISCOVERED in real test:
- WB API v9 (/exactmatch/ru/common/v9/search) теперь возвращает только метаданные
  (name, query, shardKey, filters, search_result={}) — products пусто
- WB API v18 (/exactmatch/ru/common/v18/search) — рабочий
  Структура: {metadata, products, total} — products НА ВЕРХНЕМ уровне (не data.products)
- Подтверждено: query='Haier холодильник' → 100 products via v18

CHANGES:
1. _SEARCH_URL → v18 endpoint
2. Парсинг products: сначала data.products (legacy fallback), потом products top-level
3. _build_item: цены теперь читаются из sizes[].price.{product, total, basic}
   (v18 формат), с fallback на priceU/salePriceU (v9 legacy)
4. _generate_query_variants: добавлен brand+category fallback
   ('Bosch холодильник' если не нашли по модели)

TEST: Haier холодильник → 100 results (first: 'Холодильник двухкамерный C2F619CFU1')
2026-05-11 22:59:14 +03:00
wasrusgen
555c5568ff miniapp: 25 SVG pictograms for 7 remaining categories (style D · 3D)
PODBOR_PICTS additions (25 total):
- HOB (3): elec — induction concentric circles, gas — burners with grid + knobs, combi — split panel
- OVEN (2): built-in with niche + control panel + glass window, stove (combo unit on legs)
- DW (3): full (hidden facade with handle strip), partial (control bar on top), freestanding (full controls + door + feet)
- HOOD (7): drawer (cabinet + sliding panel), hidden (cabinet only), dome (chimney shape), inclined (angled glass), island (ceiling tubes), downdraft (counter panel rising), hob-in-combo (cooktop with center exhaust slot)
- MICROWAVE (2): built-in (in niche with window+keypad), freestanding (countertop with feet)
- COFFEE (5): built-in (display + buttons + spout + cup), free-grinder (bean hopper + display), capsule (small + capsule slot), manual (with portafilter + steam wand + pressure gauge), tap (faucet integrated into countertop)
- WASHER (3): built-in (hidden facade), under-top (control panel visible, big door), freestanding (full unit + feet + powder tray)

CONFIG wiring: all 25 pict keys referenced in podbor.config.js step 1 of each category

PREVIEW: new preview-all-picts.html shows all 25 in one page for visual review
2026-05-11 21:27:40 +03:00
wasrusgen
da8a98f34f market 2026 update: Kuppersberg budget default, Haier mid, full RF brand realism
PODBOR_SINGLE_BRAND_OPTIONS (single-brand kitchen picker):
- Premium: + Gaggenau ⚠, V-Zug ⚠, Liebherr ⚠ (all parallel-import)
- Middle: + NEFF ⚠, Haier  marked recommended
- Budget: + Kuppersberg  recommended, Maunfeld, Weissgauff, Gorenje, Hotpoint, Indesit, Midea, Candy
- Removed budget-only Бирюса/Pozis/DEXP (not popular in built-in segment)

PODBOR_BRANDS per category — fully refreshed with realistic 2026 lists:
- Coffee: Bosch/Siemens/NEFF in mid, Kuppersberg/Maunfeld in budget (instead of obscure ones)
- All categories now include Kuppersberg/Maunfeld/Weissgauff in budget tier
- Premium adds Gaggenau, V-Zug consistently

AI PROMPT — new section 'РЫНОК РФ 2026':
- Documents exact tier composition with brands and price ranges
- 'Типичный выбор клиента ЗОВ-СПб: Bosch + Haier + Maunfeld'
- Premium combo: Bosch + Miele washer (для кухонь 600к+)
- Trends: parallel import normalized, Haier #2 after Bosch, Kuppersberg builder default
- СВЧ category fading — combined ovens with microwave taking over
- Induction wins, gas only in private houses

EXAMPLES в prompt expanded:
- Haier C4F744CMG, Kuppersberg NRS 1857 X, Maunfeld MBL 88LU, Weissgauff WBI 30 ATX
- Clear 'НЕ выдуманное' guidance
2026-05-11 20:23:45 +03:00
wasrusgen
c97b8dce3c parsers: skip sponsored/ad URLs (cpc/sponsored=1) — they expire in 2-3 hours
User reported clicking matrix prices led to 'Произошла ошибка!' on OZON home page.
Cause: parsers captured /product/?sponsored=1&cpc=Jtiito95... links that died after few hours.

Fix:
- ozon.py: skip href with 'sponsored=1', '/promo/', 'cpc='. Strip query string from final URL.
- yamarket.py: skip 'sponsored=1', 'cpc=', 'advUuid' (Я.Маркет sponsored marker)
- citilink.py: strip query string from final URL (defensive)

Now matrix links go to canonical product pages that don't expire.
2026-05-11 17:20:59 +03:00
wasrusgen
ef500fa446 user feedback batch: model count, specs, manual link, dimensions, export
1. MODEL COUNT SELECTOR (strategy step):
   - new PODBOR_MODEL_COUNTS [3/5/7]
   - state.model_count default '5'
   - UI on strategy page with description (быстро/оптимально/максимум)

2. AI PROMPT EXPANDED:
   - new field: manual_search_query — for Google search instruction PDF
   - new specs object per model: dimensions_mm/volume_l/weight_kg/noise_db/energy_class/color
   - 'specs ОБЯЗАТЕЛЬНЫ для проектирования кухни' explicit rule
   - reads checklist.model_count to determine how many models per category
   - max_tokens 4000 → 8000 (room for richer responses)

3. MODEL CARD RICHER:
   - _renderSpecsBlock — characteristics in 2-col grid, dimensions highlighted
   - _renderUtilityLinks — Google search buttons for инструкция (PDF) + Схема установки
   - Specs critical for ZOV kitchen design (manager needs to verify niche fits)

4. EXPORT BUTTONS:
   - 'Скачать HTML' — generates standalone HTML with inline styles, downloads as file
   - 'Печать → PDF' — opens new window with cleaned layout + auto-prints
   - User can save as PDF via system print dialog

5. PREVIEW updated with realistic specs/manual_query for all 3 fridges
2026-05-11 17:11:30 +03:00
wasrusgen
7f417da7e0 gitignore: wb.json test artifact 2026-05-11 16:49:02 +03:00
wasrusgen
5ceffa4f69 miniapp: phone validation on intro — blocks transition with bad number
- New isValidPhone(raw): checks 11-digit Russian after normalization (8/7/+7/9-prefix)
- Intro 'Начать' button now custom click handler instead of data-go
- Validates name (non-empty) and phone (Russian format)
- Inline .field-error red message under invalid field
- .field-hint shows format help under phone input
- Haptic 'warning' feedback on invalid submit
- Phone is auto-normalized to '+7 900 123-45-67' before transition
2026-05-11 16:48:52 +03:00
wasrusgen
0f2635d5f8 dns+ozon: 4 retries with proxy rotation (residential pool has dirty IPs) 2026-05-11 16:37:28 +03:00
wasrusgen
796e20bc73 gitignore: r.json (test artifacts) 2026-05-11 16:34:51 +03:00
wasrusgen
aa569a8ed1 dns: switch to Playwright (Qrator JS challenge); ozon: fix false-positive antibot detector
- DNS: использовали httpx + proxy_pool но Qrator кидал 401 даже с residential
  → теперь Playwright + residential — браузер сам решает JS challenge
- OZON: теперь проверяем только <title>='Доступ ограничен' (точная), а не подстроку '/robotcheck/'
2026-05-11 16:34:04 +03:00
wasrusgen
b27cf02aa2 yamarket: clean React JSON noise + extract title from URL slug
Я.Маркет рендерит SnippetConstructor виджет с JSON-стейтом ВНУТРИ a-тега.
Поэтому link.get_text() возвращает мусор типа {'widgets':{...}}.

Фикс:
- copy.copy(card) и удаление <script>/<noscript>/<noframes>/<template>
- Title теперь берётся из URL slug первым приоритетом (всегда чистый)
- _slug_to_title: транслитерация и капитализация
  'bosch-kgn39ul30u-dvukhkamernyy-kholodilnik-no-frost-seryy-metallik' →
  'Bosch KGN39UL30U Двухкамерный Холодильник NoFrost Серый Металлик'
2026-05-11 16:30:34 +03:00
wasrusgen
839e775151 yamarket: rewrite for /card/{slug}/{id} URL pattern (Я.Маркет 2026)
- Old /product--{id} URLs deprecated
- Walks up from a[href*='/card/'] to nearest article/zone-div
- Extracts title from link text or h2/h3/itemprop=name
- Price: min from card text (with sanity bounds 100..10M)
- Image filters yastatic / _next placeholders
- Rating: '4.7★' or '4.7 N оценок' pattern
- Reviews: 'N отзывов' / 'N оценок'
- Stores count: 'от N магазинов / предложений'
2026-05-11 16:26:28 +03:00
wasrusgen
b1d8f3e38a gitignore: exclude .tmp_* files (accidentally committed proxy creds in previous commit)
User needs to rotate Proxys.io password after migration tests are done.
2026-05-11 16:06:05 +03:00
wasrusgen
e7f6e64e38 playwright_engine: route through proxy_pool — random residential IP per request
- New use_proxy param (default True)
- Per-request random proxy from pool
- _parse_proxy_url_for_playwright converts http://user:pass@host:port to playwright.proxy dict
2026-05-11 16:05:36 +03:00
wasrusgen
811bed31a4 backend: proxy_pool supports PROXY_LIST_FILE + format auto-conversion
- New env: PROXY_LIST_FILE — path to file with one proxy per line
- _normalize_proxy_entry accepts: http://user:pass@host:port, host:port:user:pass (Proxys.io format), host:port
- _load_from_file reads file, dedup with static list
- /api/proxy_status returns file_path, file_loaded count, sample (first 3 masked)
2026-05-11 15:52:02 +03:00
wasrusgen
d7be644aed miniapp: price comparison matrix as PRIMARY view per category
WHAT CHANGED:
- New _renderPriceMatrix(models) — table with rows=models, columns=stores
- Inserted as PRIMARY view above model cards (was secondary accordion)
- Columns dynamically include only stores that returned data
- Sticky model column (left) — scrolls horizontally on mobile
- Best price per row highlighted: green bg + ✓ badge + green text
- Empty cells: '—' if no URL, 'смотреть →' if URL but no price yet
- 'Мин' column on far right — explicit cheapest price summary

CSS:
- .report-matrix-wrap with rounded card
- Sticky col-model with box-shadow on right edge
- Cell-price.best with rgba green background
- .best-mark circle badge

PREVIEW:
- Updated mock with 3 fridges + 3 hobs across multiple stores (real pricing spread)
- Demonstrates min-price highlighting working

UX:
- User can now visually compare 'where is it cheapest' at a glance
- Tap any cell with price → opens store page
- Tap empty cell with URL → opens search in store

NEXT: same matrix can become PDF/Excel export for client briefcase
2026-05-11 14:56:41 +03:00
wasrusgen
ca342c0641 ai+report: deeper analysis — required pros/cons, category insights, source visibility
AI PROMPT (ai.py):
- Requires minimum 3 pros + 2 cons per model with NUMBERS (36 dB, 463 L, A++, не 'тихий/большой')
- New field 'reasoning' — 1-sentence why-this-model justification
- New per-category 'analysis' — 2-3 sentences about trade-offs
- Strict rules: no fake article numbers, account for parallel-import price markup
- Russian market 2026 awareness: Haier/Korting up, Bosch/Siemens ⚠

TELEGRAM FORMAT (main.py):
- Renders category analysis as italic prelude
- Lists pros/cons as bullet lists (up to 4 pros, 3 cons)
- Shows '🛒 Нашли в: OZON · Citilink · WB' line listing successful sources
- Rating + reviews + stores count line: '📊 ★ 4.7 · 1242 отзыв. · 12 магаз.'
- Direct link to best store: '🔗 Открыть в магазине'

WB PARSER:
- Generates 3 query variants per request: full → brand+model → model only
- Increases hit rate when AI search_query is too verbose
- First non-empty variant wins

MINIAPP REPORT (podbor.js + podbor.css):
- Category analysis block above models (italic, walnut left-border)
- Pros block: green tinted bg, bullet list, header 'Плюсы'
- Cons block: terracotta tinted bg, bullet list, header 'Минусы'
- Reasoning chip: 💡 italic in warm background
- Source badges with per-store price '<store> · 89 990 ₽'
- Color-coded source links: OZON blue, Citilink yellow, WB pink, Я.Маркет red, DNS orange
- 'X магазинов нашли товар' header + plural fix
- '— не найден' fallback if 0 sources

PREVIEW (preview-report.html):
- Mock updated with Haier as flagship (more relevant for 2026 RF)
- Shows analysis, reasoning, source spread (4 stores with different prices)
2026-05-11 14:34:08 +03:00
wasrusgen
4b04f2de54 miniapp: summary page hides Подключение/Вентиляция if hob/hood not picked 2026-05-11 14:26:12 +03:00
wasrusgen
80580db446 miniapp: 4 UX fixes from user feedback
1. PHONE NORMALIZATION
   - On blur (or before submit): '9001234567' -> '+7 900 123-45-67'
   - Handles 8XXX, 7XXX, +7XXX, 10-digit mobile prefixes
   - Leaves untouched if not Russian-looking number

2. BRAND LIST FOR RF 2026
   - PODBOR_SINGLE_BRAND_OPTIONS updated with realistic 2026 brands
   - Promoted: Haier, Korting, Midea, Hisense, Бирюса, Атлант, Pozis, DEXP
   - Bosch/Siemens marked with ⚠ (parallel-import)
   - Miele/Liebherr/Smeg also marked ⚠
   - PODBOR_BRANDS per-category fully refreshed

3. BUDGET ADAPTIVE HINTS
   - Hints now scale by selected categories share of full kitchen
   - Just fridge picked → 'Средний' shows ~88-175 тыс instead of 350-700к
   - Full 8 categories → original 350-700к
   - PODBOR_BUDGET_SHARES + PODBOR_BUDGET_RANGES constants

4. INFRA STEP CONDITIONAL
   - Stove power question only shown if hob category picked
   - Vent question only shown if hood category picked
   - If neither → step auto-skips to summary (with brief notice)
   - Summary 'Назад' button respects skip — goes to strategy if needed
2026-05-11 14:25:25 +03:00
wasrusgen
44281b1e07 citilink: dedup by product ID + filter Next.js placeholder images 2026-05-11 13:59:07 +03:00
wasrusgen
c5f662f53d citilink: rewrite parser to walk up from a[href*=/product/] (CSS-in-JS resistant) 2026-05-11 13:57:18 +03:00
wasrusgen
1a948ebf02 ozon: fix false-positive challenge detector (was catching 'challenge' in normal JS) 2026-05-11 13:54:13 +03:00
wasrusgen
e8b487891f backend: working parsers — OZON + Citilink (DOM via Playwright) + WB
DIAGNOSTIC RESULTS:
- OZON: 19 product links via Playwright on naked VPS-IP ✓
- Citilink: 112 data-meta-name Snippets ✓
- Wildberries: JSON API works with delays ✓
- Я.Маркет, DNS: blocked by ASN (need residential proxy)

OZON PARSER:
- Pure Playwright DOM (composer-api dropped — was blocked)
- Selects a[href*='/product/'], walks up to card div, extracts title/price/img
- Filters fake 'titles' like Распродажа, Скидка

CITILINK PARSER (new):
- Selects [data-meta-name*='Snippet'] or ProductCard markers
- Multiple title selectors fallback chain
- Filters out non-product hits

PARSERS/__init__.py:
- DEFAULT_SOURCES = (ozon, citilink, wb) — all work without proxy
- Я.Маркет, DNS kept but not default — usable when residential proxy added

NEW ENDPOINT:
- GET /api/parse_citilink?q=...&limit=N
2026-05-11 13:53:07 +03:00