From 206343895e592c47580b744fa89303ea05cb8bc8 Mon Sep 17 00:00:00 2001 From: IK Date: Mon, 21 Sep 2026 19:54:06 +0700 Subject: [PATCH] chore: call rigs assemblies in UI --- apps/web/src/components/CatchList.astro | 2 +- apps/web/src/lib/media.ts | 2 +- apps/web/src/pages/media.astro | 2 +- apps/web/src/pages/tackle/analytics.astro | 2 +- apps/web/src/pages/tackle/index.astro | 6 +++--- apps/web/src/pages/tackle/items/[id].astro | 2 +- apps/web/src/pages/tackle/rigs/[id].astro | 14 +++++++------- docs/ROADMAP.md | 8 ++++---- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/apps/web/src/components/CatchList.astro b/apps/web/src/components/CatchList.astro index 3845ceb..3b21cac 100644 --- a/apps/web/src/components/CatchList.astro +++ b/apps/web/src/components/CatchList.astro @@ -4,7 +4,7 @@ import { ago, kg, type Catch } from "../lib/api"; import TackleGlyph from "./TackleGlyph.astro"; const { catches } = Astro.props as { catches: Catch[] }; const methodLabels: Record = { spinning: "Спиннинг", bottom: "Донная", float: "Поплавочная" }; -const roleLabels: Record = { lure: "Приманка", bait: "Наживка", rig: "Монтаж", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", float: "Поплавок", sinker: "Груз" }; +const roleLabels: Record = { lure: "Приманка", bait: "Наживка", rig: "Сборка", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", float: "Поплавок", sinker: "Груз" }; ---
{catches.map(item => { diff --git a/apps/web/src/lib/media.ts b/apps/web/src/lib/media.ts index 1b40321..4b0e974 100644 --- a/apps/web/src/lib/media.ts +++ b/apps/web/src/lib/media.ts @@ -7,7 +7,7 @@ const mediaRoleLabels: Record = { waterbody_screenshot: "Скриншот водоёма", tackle_card: "Карточка снасти", tackle_detail: "Деталь снасти", - rig_diagram: "Схема монтажа", + rig_diagram: "Схема сборки", tackle_screenshot: "Скриншот снасти", }; diff --git a/apps/web/src/pages/media.astro b/apps/web/src/pages/media.astro index cfae657..23d2721 100644 --- a/apps/web/src/pages/media.astro +++ b/apps/web/src/pages/media.astro @@ -8,7 +8,7 @@ const allowed = new Set(["fish", "waterbody", "tackle", "reference"]); const requested = Astro.url.searchParams.get("type") ?? "fish"; const type = allowed.has(requested) ? requested : "fish"; const requestedRole = Astro.url.searchParams.get("role") ?? ""; -const roleOptions: Record> = { waterbody: [["waterbody_cover", "Заставки"], ["waterbody_map", "Карты"], ["waterbody_depth_map", "Карты глубин"], ["waterbody_screenshot", "Скриншоты"]], tackle: [["tackle_card", "Карточки снастей"], ["tackle_detail", "Детали снастей"], ["rig_diagram", "Схемы монтажей"], ["tackle_screenshot", "Скриншоты снастей"]] }; +const roleOptions: Record> = { waterbody: [["waterbody_cover", "Заставки"], ["waterbody_map", "Карты"], ["waterbody_depth_map", "Карты глубин"], ["waterbody_screenshot", "Скриншоты"]], tackle: [["tackle_card", "Карточки снастей"], ["tackle_detail", "Детали снастей"], ["rig_diagram", "Схемы сборок"], ["tackle_screenshot", "Скриншоты снастей"]] }; const selectedRole = (roleOptions[type] ?? []).some(([value]) => value === requestedRole) ? requestedRole : ""; let assets: MediaAsset[] = [], unavailable = false; try { const query = new URLSearchParams({ entity_type: type }); if (selectedRole) query.set("media_role", selectedRole); assets = await api(`/api/v1/media/catalog?${query}`); } catch { unavailable = true; } diff --git a/apps/web/src/pages/tackle/analytics.astro b/apps/web/src/pages/tackle/analytics.astro index fe0d52d..3e97b3a 100644 --- a/apps/web/src/pages/tackle/analytics.astro +++ b/apps/web/src/pages/tackle/analytics.astro @@ -25,7 +25,7 @@ try { } catch { unavailable = true; } if (unavailable) { Astro.response.status = 503; Astro.response.headers.set("Retry-After", "60"); Astro.response.headers.set("Cache-Control", "no-store"); } const methodLabels: Record = { spinning: "Спиннинг", bottom: "Донная", float: "Поплавочная" }; -const roleLabels: Record = { lure: "Приманка", bait: "Наживка", rig: "Монтаж", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", float: "Поплавок", sinker: "Груз" }; +const roleLabels: Record = { lure: "Приманка", bait: "Наживка", rig: "Сборка", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", float: "Поплавок", sinker: "Груз" }; --- diff --git a/apps/web/src/pages/tackle/index.astro b/apps/web/src/pages/tackle/index.astro index f04f2ab..78597c1 100644 --- a/apps/web/src/pages/tackle/index.astro +++ b/apps/web/src/pages/tackle/index.astro @@ -28,14 +28,14 @@ const [itemsResult, rigsResult] = await Promise.allSettled([ if (itemsResult.status === "fulfilled") result = itemsResult.value; else unavailable = true; if (rigsResult.status === "fulfilled") rigs = rigsResult.value; else rigsUnavailable = true; if (unavailable) { Astro.response.status = 503; Astro.response.headers.set("Retry-After", "60"); Astro.response.headers.set("Cache-Control", "no-store"); } -const categoryLabels: Record = { bait: "Наживка", lure: "Приманка", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", rig: "Монтаж", float: "Поплавок", sinker: "Груз", other: "Другое" }; +const categoryLabels: Record = { bait: "Наживка", lure: "Приманка", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", rig: "Сборка", float: "Поплавок", sinker: "Груз", other: "Другое" }; const filterParams = new URLSearchParams(); if (category) filterParams.set("category", category); if (search) filterParams.set("q", search); if (brand) filterParams.set("brand", brand); if (family) filterParams.set("family", family); --- - +
@@ -47,6 +47,6 @@ if (family) filterParams.set("family", family); Сочетания снастей
{unavailable ? : result.items.length ?
{result.items.map(item => {categoryLabels[item.category] ?? item.category}{item.name}{[item.brand, item.family].filter(Boolean).join(" · ") || "Характеристики уточняются"}
)}
: } - {!unavailable &&
Сборки снастей

Монтажи

{rigs.total} всего
{rigsUnavailable ? : rigs.items.length ? : }
} + {!unavailable &&
Сборки снастей

Сборки

{rigs.total} всего
{rigsUnavailable ? : rigs.items.length ? : }
} {!unavailable && }
diff --git a/apps/web/src/pages/tackle/items/[id].astro b/apps/web/src/pages/tackle/items/[id].astro index 052dd01..495b775 100644 --- a/apps/web/src/pages/tackle/items/[id].astro +++ b/apps/web/src/pages/tackle/items/[id].astro @@ -23,7 +23,7 @@ if (unavailable) { Astro.response.headers.set("Cache-Control", "no-store"); } -const categoryLabels: Record = { bait: "Наживка", lure: "Приманка", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", rig: "Монтаж", float: "Поплавок", sinker: "Груз", other: "Другое" }; +const categoryLabels: Record = { bait: "Наживка", lure: "Приманка", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", rig: "Сборка", float: "Поплавок", sinker: "Груз", other: "Другое" }; const missingLabels: Record = { subcategory: "подкатегория", brand: "бренд", family: "семейство", unlock_level: "уровень открытия", source_url: "ссылка на источник", source_checked_at: "дата проверки" }; const missing = item?.missing_fields.map((field) => missingLabels[field] ?? field) ?? []; const sourceHref = safeHttpUrl(item?.source_url); diff --git a/apps/web/src/pages/tackle/rigs/[id].astro b/apps/web/src/pages/tackle/rigs/[id].astro index 0d9962f..3178a56 100644 --- a/apps/web/src/pages/tackle/rigs/[id].astro +++ b/apps/web/src/pages/tackle/rigs/[id].astro @@ -23,17 +23,17 @@ if (unavailable) { Astro.response.headers.set("Cache-Control", "no-store"); } -const roleLabels: Record = { lure: "Приманка", bait: "Наживка", rig: "Монтаж", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", float: "Поплавок", sinker: "Груз" }; +const roleLabels: Record = { lure: "Приманка", bait: "Наживка", rig: "Сборка", rod: "Удилище", reel: "Катушка", line: "Леска", hook: "Крючок", float: "Поплавок", sinker: "Груз" }; const missingLabels: Record = { source_url: "ссылка на источник", source_checked_at: "дата проверки" }; const missing = rig?.missing_fields.map((field) => missingLabels[field] ?? field) ?? []; const sourceHref = safeHttpUrl(rig?.source_url); --- - - - - {unavailable ? : !rig ? : <> -
-
Монтаж

{rig.name}

{rig.source_external_id ? `Идентификатор источника: ${rig.source_external_id}` : "Внешний идентификатор не указан."}

+ + + + {unavailable ? : !rig ? : <> +
+
Сборка

{rig.name}

{rig.source_external_id ? `Идентификатор источника: ${rig.source_external_id}` : "Внешний идентификатор не указан."}

Компоненты
    {rig.components.length ? rig.components.map(component =>
  1. {roleLabels[component.role] ?? component.role}{component.tackle_item_id ? {component.raw_value ?? "Карточка снасти"} : {component.raw_value ?? "Значение не указано"}}
  2. ) :
  3. Состав не указан.
  4. }
{missing.length > 0 &&

Не хватает: {missing.join(", ")}.

}{sourceHref && Открыть первоисточник}
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 193af98..7360138 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -63,16 +63,16 @@ Этот пакет повторяет жизненный цикл W01–W08, но не смешивает разные уровни описания. Наживка/приманка — предмет, который указан в улове; снасть — компонент комплекта (удилище, катушка, леска, крючок и т. п.); оснастка — -собранная схема или монтаж (например, донная, поплавочная, method). Число +собранная схема или сборка (например, донная, поплавочная, method). Число найденных изображений приманок не считается размером полного каталога. - [ ] **G01 · Canonical-каталог RF4DB.** Offline-контракт и строгий parser готовы для категорий `bait`, `lure`, `rod`, `reel`, `line`, `hook`, `rig`, `float`, `sinker`, `other`: сохраняются исходный slug/ID, русское название, категория, подкатегория, бренд, семейство, игровой уровень и source URL; duplicate ID и неизвестная категория отклоняются. Общий count остаётся `unknown`, пока не получен разрешённый реальный индекс; fixture не считается каталогом. -- [ ] **G02 · Карточки предметов и оснасток.** Fixture-based parser списка и detail-страницы готов: характеристики, варианты, совместимость, изображения и связанные типы монтажа сохраняются; атрибуты различают фактический `0`, `not_applicable` и `missing`. Parser fail-closed при неполном/изменившемся DOM; реальные detail-запросы выполнять только для выбранных карточек и с общим 30-минутным cooldown домена. +- [ ] **G02 · Карточки предметов и оснасток.** Fixture-based parser списка и detail-страницы готов: характеристики, варианты, совместимость, изображения и связанные типы сборок сохраняются; атрибуты различают фактический `0`, `not_applicable` и `missing`. Parser fail-closed при неполном/изменившемся DOM; реальные detail-запросы выполнять только для выбранных карточек и с общим 30-минутным cooldown домена. - [ ] **G03 · Модель и provenance.** Добавлены отдельные `tackle_item`, `rig` и `rig_component` с миграцией `0021`; legacy `bait` и `catch_report.bait_id` не изменялись. `tackle_item` хранит `category`, `subcategory`, `brand`, `family`, `unlock_level`, source identity, timestamp и `raw_payload`; `rig_component` хранит роль, порядок, исходное значение и optional canonical item. Связи с catch-потоком добавлены в G05; безопасный backfill остаётся только после реального crosswalk. - [ ] **G04 · Crosswalk и нормализация.** Добавлен offline `gear_crosswalk`: нормализация регистра/пробелов/`е/ё`, точное имя или alias плюс совместимая категория, консервативная проверка brand/family. Неоднозначные, несовместимые, брендовые и unmatched-строки получают review-статус без canonical key; исходное значение сохраняется. Остаётся подать реальные RF4DB/RF4MAP/RF4 Posts identities и вручную подтвердить результаты. - [ ] **G05 · Связи с уловами и источниками.** Добавлены `catch_tackle_component` и offline `gear_components`: можно сохранять несколько unresolved/canonical компонентов с ролью, порядком, исходным значением, source identity и `raw_payload`; legacy `bait_id` не меняется. Parser сохраняет порядок оборудования из detail и разделяет bait/rig в catch-полях. Запись компонентов подключена к community import, официальному импорту и пользовательской форме; повторная обработка идемпотентна. Canonical-привязка и безопасный backfill остаются только после подтверждённого crosswalk. -- [ ] **G06 · API и публичный каталог.** Добавлены пагинированные `/api/v1/tackle/items` и `/api/v1/tackle/rigs` с фильтрами предметов и монтажей по названию, а также предметов по категории, бренду, семейству и уровню; detail endpoints для предмета и монтажа, а также ordered `tackle_components` в ответе уловов точки. Ответы показывают только канонические характеристики, provenance, timestamp проверки и `missing_fields`; рейтинг эффективности не добавляется. Публичный каталог теперь показывает обнаруживаемые карточки снастей и монтажей, имеет detail-routes `/tackle/items/:id` и `/tackle/rigs/:id` с 404/unavailable-различением, паспортом данных и явным списком недостающих полей; CatchList показывает ordered-компоненты и ведёт в canonical detail снасти или монтажа только при наличии соответствующего ID, а unresolved raw value оставляет текстом. -- [ ] **G07 · Аналитика сочетаний и рекомендации.** Добавлен `/api/v1/analytics/tackle`: approved-наблюдения группируются по роли и исходному компоненту с фильтрами водоёма, рыбы, метода, роли и окна; дубликаты одного улова не увеличивают счётчик, а минимум наблюдений и независимых игроков отделяет факт использования от рекомендации. Публичный `/tackle/analytics` показывает и сохраняет фильтры, имеет clean reset-link, выборку, независимых игроков и раздельные `recommendation`/`insufficient_data` states без рейтинга эффективности. Добавлен объяснимый `freshness_score` с half-life `12.5` часа и контрактом `0..100`: он не меняет raw-счётчики и пороги, но стабильно ставит более свежую равную выборку выше старой и виден на карточке. При однозначной canonical-привязке аналитика ведёт в карточку снасти или монтажа, а при конфликте идентификаторов сохраняет raw-значение без ложной ссылки. Остаётся дальнейшая визуальная приёмка. +- [ ] **G06 · API и публичный каталог.** Добавлены пагинированные `/api/v1/tackle/items` и `/api/v1/tackle/rigs` с фильтрами предметов и сборок по названию, а также предметов по категории, бренду, семейству и уровню; detail endpoints для предмета и сборки, а также ordered `tackle_components` в ответе уловов точки. Ответы показывают только канонические характеристики, provenance, timestamp проверки и `missing_fields`; рейтинг эффективности не добавляется. Публичный каталог теперь показывает обнаруживаемые карточки снастей и сборок, имеет detail-routes `/tackle/items/:id` и `/tackle/rigs/:id` с 404/unavailable-различением, паспортом данных и явным списком недостающих полей; CatchList показывает ordered-компоненты и ведёт в canonical detail снасти или сборки только при наличии соответствующего ID, а unresolved raw value оставляет текстом. +- [ ] **G07 · Аналитика сочетаний и рекомендации.** Добавлен `/api/v1/analytics/tackle`: approved-наблюдения группируются по роли и исходному компоненту с фильтрами водоёма, рыбы, метода, роли и окна; дубликаты одного улова не увеличивают счётчик, а минимум наблюдений и независимых игроков отделяет факт использования от рекомендации. Публичный `/tackle/analytics` показывает и сохраняет фильтры, имеет clean reset-link, выборку, независимых игроков и раздельные `recommendation`/`insufficient_data` states без рейтинга эффективности. Добавлен объяснимый `freshness_score` с half-life `12.5` часа и контрактом `0..100`: он не меняет raw-счётчики и пороги, но стабильно ставит более свежую равную выборку выше старой и виден на карточке. При однозначной canonical-привязке аналитика ведёт в карточку снасти или сборки, а при конфликте идентификаторов сохраняет raw-значение без ложной ссылки. Остаётся дальнейшая визуальная приёмка. - [ ] **G08 · Медиа и качество.** Добавлены отдельные reviewed-роли `tackle_card`, `tackle_detail`, `rig_diagram`, `tackle_screenshot` для `tackle`, а также CLI-параметр `--media-role`; offline audit отклоняет неизвестную роль и несовпадение роли с entity type. Существующие dimensions, MIME, SHA-256, прозрачность, aspect ratio, provenance и атомарное продвижение сохраняются. Публичный и admin UI используют единый словарь человекочитаемых role labels с безопасным fallback для будущих ролей; публичная медиатека фильтрует разрешённые роли через URL/API, admin-карточки дополнительно получают `data-media-role` для проверяемой инспекции, unit-регрессия словаря проходит. Остаётся провести реальный contact-sheet review для будущих tackle-кандидатов без автоматической публикации. - [ ] **G09 · Приёмка и эксплуатация.** Добавлены fixture/regression tests для crosswalk, media roles, идемпотентных компонентов, пустых результатов и многокомпонентных наблюдений; каталог `/tackle` включён в visual-matrix, narrow smoke и accessibility routes. Offline catalog/media audits и сохранение старых данных при сбое импорта проходят. TEMP-only query-plan gate для фильтра каталога и группировки сочетаний использует индексы и укладывается в 250 мс. Chromium подтвердил empty-state публичного каталога и отсутствие overflow на 320 px; обычный E2E пропускает bootstrap без явных переменных, отдельный bootstrap с токеном проходит. Остаются HTTP/browser acceptance для неоднозначных и многокомпонентных комплектов, ручной visual review и проверяемый счётчик по категориям либо явный `unknown`. Сетевые тесты не выполнять; импорт оставить opt-in, последовательным и под общим cooldown/backoff.